mirror of
https://github.com/Iconica-Development/flutter_shopping.git
synced 2025-05-19 17:03:45 +02:00
Compare commits
No commits in common. "master" and "1.0.0" have entirely different histories.
127 changed files with 4892 additions and 3563 deletions
|
@ -1,8 +1,3 @@
|
||||||
## 2.0.0
|
|
||||||
- Added `flutter_shopping_interface` package
|
|
||||||
- Implemented default design
|
|
||||||
|
|
||||||
|
|
||||||
## 1.0.0
|
## 1.0.0
|
||||||
|
|
||||||
- Initial version of the combined melos variant of the flutter_shopping user-story.
|
- Initial version of the combined melos variant of the flutter_shopping user-story.
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
/// Flutter component for shopping cart.
|
/// Flutter component for shopping cart.
|
||||||
library flutter_order_details;
|
library flutter_order_details;
|
||||||
|
|
||||||
export "package:flutter_form_wizard/flutter_form.dart";
|
|
||||||
|
|
||||||
export "src/configuration/order_detail_configuration.dart";
|
export "src/configuration/order_detail_configuration.dart";
|
||||||
export "src/configuration/order_detail_translations.dart";
|
export "src/configuration/order_detail_localization.dart";
|
||||||
export "src/order_detail_screen.dart";
|
export "src/configuration/order_detail_step.dart";
|
||||||
export "src/widgets/order_succes.dart";
|
export "src/configuration/order_detail_title_style.dart";
|
||||||
|
export "src/models/order_address_input.dart";
|
||||||
|
export "src/models/order_choice_input.dart";
|
||||||
|
export "src/models/order_dropdown_input.dart";
|
||||||
|
export "src/models/order_email_input.dart";
|
||||||
|
export "src/models/order_input.dart";
|
||||||
|
export "src/models/order_phone_input.dart";
|
||||||
|
export "src/models/order_result.dart";
|
||||||
|
export "src/models/order_text_input.dart";
|
||||||
|
export "src/models/order_time_picker_input.dart";
|
||||||
|
export "src/widgets/order_detail_screen.dart";
|
||||||
|
|
|
@ -1,71 +1,50 @@
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/widgets.dart";
|
||||||
import "package:flutter_order_details/flutter_order_details.dart";
|
import "package:flutter_order_details/src/configuration/order_detail_localization.dart";
|
||||||
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
|
import "package:flutter_order_details/src/configuration/order_detail_step.dart";
|
||||||
|
import "package:flutter_order_details/src/models/order_result.dart";
|
||||||
|
|
||||||
/// Configuration for the order detail screen.
|
/// Configuration for the order detail screen.
|
||||||
class OrderDetailConfiguration {
|
class OrderDetailConfiguration {
|
||||||
/// Constructor for the order detail configuration.
|
/// Constructor for the order detail configuration.
|
||||||
const OrderDetailConfiguration({
|
const OrderDetailConfiguration({
|
||||||
required this.shoppingService,
|
required this.steps,
|
||||||
required this.onNextStep,
|
//
|
||||||
required this.onStepsCompleted,
|
required this.onCompleted,
|
||||||
required this.onCompleteOrderDetails,
|
//
|
||||||
this.pages,
|
this.progressIndicator = true,
|
||||||
this.translations = const OrderDetailTranslations(),
|
//
|
||||||
this.appBarBuilder,
|
this.localization = const OrderDetailLocalization(),
|
||||||
this.nextbuttonBuilder,
|
//
|
||||||
this.orderSuccessBuilder,
|
this.inputFieldPadding = const EdgeInsets.symmetric(
|
||||||
|
horizontal: 32,
|
||||||
|
vertical: 16,
|
||||||
|
),
|
||||||
|
this.titlePadding = const EdgeInsets.only(left: 16, right: 16, top: 16),
|
||||||
|
//
|
||||||
|
this.appBar,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The shopping service that is used
|
|
||||||
final ShoppingService shoppingService;
|
|
||||||
|
|
||||||
/// The different steps that the user has to go through to complete the order.
|
/// 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.
|
/// Each step contains a list of fields that the user has to fill in.
|
||||||
final List<FlutterFormPage> Function(BuildContext context)? pages;
|
final List<OrderDetailStep> steps;
|
||||||
|
|
||||||
/// Callback function that is called when the user has completed the order.
|
/// 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.
|
/// The result of the order is passed as an argument to the function.
|
||||||
final Function(
|
final Function(OrderResult result) onCompleted;
|
||||||
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.
|
/// Whether or not you want to show a progress indicator at
|
||||||
final Function(
|
/// the top of the screen.
|
||||||
int currentStep,
|
final bool progressIndicator;
|
||||||
Map<String, dynamic> data,
|
|
||||||
FlutterFormController controller,
|
|
||||||
) onNextStep;
|
|
||||||
|
|
||||||
/// Localization for the order detail screen.
|
/// Localization for the order detail screen.
|
||||||
final OrderDetailTranslations translations;
|
final OrderDetailLocalization localization;
|
||||||
|
|
||||||
|
/// Padding around the input fields.
|
||||||
|
final EdgeInsets inputFieldPadding;
|
||||||
|
|
||||||
|
/// Padding around the title of the input fields.
|
||||||
|
final EdgeInsets titlePadding;
|
||||||
|
|
||||||
/// Optional app bar that you can pass to the order detail screen.
|
/// Optional app bar that you can pass to the order detail screen.
|
||||||
final PreferredSizeWidget? Function(BuildContext context, String title)?
|
final PreferredSizeWidget? appBar;
|
||||||
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 +1,18 @@
|
||||||
/// Localizations for the order detail page.
|
/// Localizations for the order detail page.
|
||||||
class OrderDetailTranslations {
|
class OrderDetailLocalization {
|
||||||
/// Constructor for the order detail localization.
|
/// Constructor for the order detail localization.
|
||||||
const OrderDetailTranslations({
|
const OrderDetailLocalization({
|
||||||
this.nextButton = "Order",
|
this.nextButton = "Next",
|
||||||
|
this.backButton = "Back",
|
||||||
this.completeButton = "Complete",
|
this.completeButton = "Complete",
|
||||||
this.orderDetailsTitle = "Information",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Next button localization.
|
/// Next button localization.
|
||||||
final String nextButton;
|
final String nextButton;
|
||||||
|
|
||||||
|
/// Back button localization.
|
||||||
|
final String backButton;
|
||||||
|
|
||||||
/// Complete button localization.
|
/// Complete button localization.
|
||||||
final String completeButton;
|
final String completeButton;
|
||||||
|
|
||||||
/// Title for the order details page.
|
|
||||||
final String orderDetailsTitle;
|
|
||||||
}
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import "package:flutter/widgets.dart";
|
||||||
|
import "package:flutter_order_details/flutter_order_details.dart";
|
||||||
|
|
||||||
|
/// Configuration for the order detail step.
|
||||||
|
class OrderDetailStep {
|
||||||
|
/// Constructor for the order detail step.
|
||||||
|
OrderDetailStep({
|
||||||
|
required this.formKey,
|
||||||
|
required this.fields,
|
||||||
|
this.stepName,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Optional name for the step.
|
||||||
|
final String? stepName;
|
||||||
|
|
||||||
|
/// Form key for the step.
|
||||||
|
final GlobalKey<FormState> formKey;
|
||||||
|
|
||||||
|
/// List of fields that the user has to fill in.
|
||||||
|
/// Each field must extend from the `OrderDetailInput` class.
|
||||||
|
final List<OrderDetailInput> fields;
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
/// An enum to define the style of the title in the order detail.
|
||||||
|
enum OrderDetailTitleStyle {
|
||||||
|
/// The title displayed as a textlabel above the field.
|
||||||
|
text,
|
||||||
|
|
||||||
|
/// The title displayed as a label inside the field.
|
||||||
|
/// NOTE: Not all fields support this. Such as, but not limited to:
|
||||||
|
/// - Dropdown
|
||||||
|
/// - Time Picker
|
||||||
|
label,
|
||||||
|
|
||||||
|
/// Does not display any form of title.
|
||||||
|
none,
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
|
/// Error Builder for form fields.
|
||||||
|
class FormFieldErrorBuilder extends StatelessWidget {
|
||||||
|
/// Constructor for the form field error builder.
|
||||||
|
const FormFieldErrorBuilder({
|
||||||
|
required this.errorMessage,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Error message to display.
|
||||||
|
final String errorMessage;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
return Text(
|
||||||
|
errorMessage,
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.colorScheme.error,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,160 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter/services.dart";
|
||||||
|
import "package:flutter_order_details/flutter_order_details.dart";
|
||||||
|
|
||||||
|
/// Order input for addresses with with predefined text fields and validation.
|
||||||
|
class OrderAddressInput extends OrderDetailInput<String> {
|
||||||
|
/// Constructor of the order address input.
|
||||||
|
OrderAddressInput({
|
||||||
|
required super.title,
|
||||||
|
required super.outputKey,
|
||||||
|
required this.textController,
|
||||||
|
super.titleStyle,
|
||||||
|
super.titleAlignment,
|
||||||
|
super.titlePadding,
|
||||||
|
super.subtitle,
|
||||||
|
super.errorIsRequired,
|
||||||
|
super.hint = "0000XX",
|
||||||
|
super.isRequired,
|
||||||
|
super.isReadOnly,
|
||||||
|
super.initialValue,
|
||||||
|
this.streetNameTitle = "Street name",
|
||||||
|
this.postalCodeTitle = "Postal code",
|
||||||
|
this.cityTitle = "City",
|
||||||
|
this.streetNameValidators,
|
||||||
|
this.postalCodeValidators,
|
||||||
|
this.cityValidators,
|
||||||
|
this.inputFormatters,
|
||||||
|
super.paddingBetweenFields = const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Title for the street name.
|
||||||
|
final String streetNameTitle;
|
||||||
|
|
||||||
|
/// Title for the postal code.
|
||||||
|
final String postalCodeTitle;
|
||||||
|
|
||||||
|
/// Title for the city.
|
||||||
|
final String cityTitle;
|
||||||
|
|
||||||
|
/// Text Control parent that contains the value of all the other three
|
||||||
|
/// controllers.
|
||||||
|
final TextEditingController textController;
|
||||||
|
|
||||||
|
/// Text Controller for street names.
|
||||||
|
final TextEditingController streetNameController = TextEditingController();
|
||||||
|
|
||||||
|
/// Text Controller for postal codes.
|
||||||
|
final TextEditingController postalCodeController = TextEditingController();
|
||||||
|
|
||||||
|
/// Text Controller for the city name.
|
||||||
|
final TextEditingController cityController = TextEditingController();
|
||||||
|
|
||||||
|
/// Validators for the street name.
|
||||||
|
final List<String? Function(String?)>? streetNameValidators;
|
||||||
|
|
||||||
|
/// Validators for the postal code.
|
||||||
|
final List<String? Function(String?)>? postalCodeValidators;
|
||||||
|
|
||||||
|
/// Validators for the city.
|
||||||
|
final List<String? Function(String?)>? cityValidators;
|
||||||
|
|
||||||
|
/// Input formatters for the postal code.
|
||||||
|
final List<TextInputFormatter>? inputFormatters;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(
|
||||||
|
BuildContext context,
|
||||||
|
String? buildInitialValue,
|
||||||
|
Function({bool needsBlur}) onBlurBackground,
|
||||||
|
) {
|
||||||
|
void setUpControllers(String address) {
|
||||||
|
var addressParts = address.split(", ");
|
||||||
|
|
||||||
|
if (addressParts.isNotEmpty) {
|
||||||
|
streetNameController.text = addressParts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addressParts.length > 1) {
|
||||||
|
postalCodeController.text = addressParts[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addressParts.length > 2) {
|
||||||
|
cityController.text = addressParts[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void inputChanged(String _) {
|
||||||
|
var address = "${streetNameController.text}, "
|
||||||
|
"${postalCodeController.text}, "
|
||||||
|
"${cityController.text}";
|
||||||
|
|
||||||
|
textController.text = address;
|
||||||
|
|
||||||
|
currentValue = address;
|
||||||
|
onValueChanged?.call(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
textController.text = initialValue ?? buildInitialValue ?? "";
|
||||||
|
currentValue = textController.text;
|
||||||
|
|
||||||
|
setUpControllers(currentValue ?? "");
|
||||||
|
|
||||||
|
return buildOutline(
|
||||||
|
context,
|
||||||
|
[
|
||||||
|
OrderTextInput(
|
||||||
|
title: streetNameTitle,
|
||||||
|
outputKey: "internal_street_name",
|
||||||
|
textController: streetNameController,
|
||||||
|
titleStyle: OrderDetailTitleStyle.none,
|
||||||
|
onValueChanged: inputChanged,
|
||||||
|
hint: "De Dam 1",
|
||||||
|
initialValue: streetNameController.text,
|
||||||
|
validators: streetNameValidators ?? [],
|
||||||
|
),
|
||||||
|
OrderTextInput(
|
||||||
|
title: postalCodeTitle,
|
||||||
|
outputKey: "internal_postal_code",
|
||||||
|
textController: postalCodeController,
|
||||||
|
titleStyle: OrderDetailTitleStyle.none,
|
||||||
|
onValueChanged: inputChanged,
|
||||||
|
validators: postalCodeValidators ??
|
||||||
|
[
|
||||||
|
(value) {
|
||||||
|
if (value?.length != 6) {
|
||||||
|
return "Postal code must be 6 characters";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
(value) {
|
||||||
|
if (value != null &&
|
||||||
|
!RegExp(r"^\d{4}\s?[a-zA-Z]{2}$").hasMatch(value)) {
|
||||||
|
return "Postal code must be in the format 0000XX";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inputFormatters: inputFormatters ??
|
||||||
|
[
|
||||||
|
FilteringTextInputFormatter.allow(RegExp(r"^\d{0,4}[A-Z]*")),
|
||||||
|
LengthLimitingTextInputFormatter(6),
|
||||||
|
],
|
||||||
|
hint: hint,
|
||||||
|
initialValue: postalCodeController.text,
|
||||||
|
),
|
||||||
|
OrderTextInput(
|
||||||
|
title: cityTitle,
|
||||||
|
outputKey: "internal_city",
|
||||||
|
textController: cityController,
|
||||||
|
titleStyle: OrderDetailTitleStyle.none,
|
||||||
|
onValueChanged: inputChanged,
|
||||||
|
hint: "Amsterdam",
|
||||||
|
initialValue: cityController.text,
|
||||||
|
validators: cityValidators ?? [],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onBlurBackground,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,175 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_order_details/flutter_order_details.dart";
|
||||||
|
import "package:flutter_order_details/src/models/formfield_error_builder.dart";
|
||||||
|
|
||||||
|
/// Order input for choice with predefined text fields and validation.
|
||||||
|
class OrderChoiceInput extends OrderDetailInput<String> {
|
||||||
|
/// Constructor of the order choice input.
|
||||||
|
OrderChoiceInput({
|
||||||
|
required super.title,
|
||||||
|
required super.outputKey,
|
||||||
|
required this.items,
|
||||||
|
super.titleStyle,
|
||||||
|
super.titleAlignment,
|
||||||
|
super.titlePadding,
|
||||||
|
super.subtitle,
|
||||||
|
super.errorIsRequired,
|
||||||
|
super.isRequired,
|
||||||
|
super.isReadOnly,
|
||||||
|
super.initialValue,
|
||||||
|
this.fieldHeight = 140,
|
||||||
|
this.fieldPadding = const EdgeInsets.symmetric(
|
||||||
|
horizontal: 4,
|
||||||
|
vertical: 64,
|
||||||
|
),
|
||||||
|
this.paddingBetweenFields = const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Items to show within the dropdown menu.
|
||||||
|
final List<String> items;
|
||||||
|
|
||||||
|
/// Padding for the field.
|
||||||
|
final EdgeInsets fieldPadding;
|
||||||
|
|
||||||
|
/// Padding between fields.
|
||||||
|
@override
|
||||||
|
// ignore: overridden_fields
|
||||||
|
final EdgeInsets paddingBetweenFields;
|
||||||
|
|
||||||
|
/// The height of the input field.
|
||||||
|
final double fieldHeight;
|
||||||
|
|
||||||
|
final _ChoiceNotifier _notifier = _ChoiceNotifier();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(
|
||||||
|
BuildContext context,
|
||||||
|
String? buildInitialValue,
|
||||||
|
Function({bool needsBlur}) onBlurBackground,
|
||||||
|
) {
|
||||||
|
void onItemChanged(String value) {
|
||||||
|
if (value == currentValue) {
|
||||||
|
currentValue = null;
|
||||||
|
onValueChanged?.call("");
|
||||||
|
_notifier.setValue("");
|
||||||
|
} else {
|
||||||
|
currentValue = value;
|
||||||
|
onValueChanged?.call(value);
|
||||||
|
_notifier.setValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildOutline(
|
||||||
|
context,
|
||||||
|
ListenableBuilder(
|
||||||
|
listenable: _notifier,
|
||||||
|
builder: (context, child) => _ChoiceInputField(
|
||||||
|
currentValue: currentValue ?? initialValue ?? buildInitialValue ?? "",
|
||||||
|
items: items,
|
||||||
|
onTap: onItemChanged,
|
||||||
|
validate: validate,
|
||||||
|
fieldPadding: fieldPadding,
|
||||||
|
paddingBetweenFields: paddingBetweenFields,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onBlurBackground,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChoiceNotifier extends ChangeNotifier {
|
||||||
|
String? _value;
|
||||||
|
|
||||||
|
String? get value => _value;
|
||||||
|
|
||||||
|
void setValue(String value) {
|
||||||
|
_value = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChoiceInputField<T> extends FormField<T> {
|
||||||
|
_ChoiceInputField({
|
||||||
|
required T currentValue,
|
||||||
|
required List<T> items,
|
||||||
|
required Function(T) onTap,
|
||||||
|
required String? Function(T?) validate,
|
||||||
|
required EdgeInsets fieldPadding,
|
||||||
|
required EdgeInsets paddingBetweenFields,
|
||||||
|
super.key,
|
||||||
|
}) : super(
|
||||||
|
validator: (value) => validate(currentValue),
|
||||||
|
builder: (FormFieldState<T> field) => Padding(
|
||||||
|
padding: fieldPadding,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
for (var item in items) ...[
|
||||||
|
Padding(
|
||||||
|
padding: paddingBetweenFields,
|
||||||
|
child: _InputContent<T>(
|
||||||
|
i: item,
|
||||||
|
currentValue: currentValue,
|
||||||
|
onTap: onTap,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (field.hasError) ...[
|
||||||
|
FormFieldErrorBuilder(errorMessage: field.errorText!),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InputContent<T> extends StatelessWidget {
|
||||||
|
const _InputContent({
|
||||||
|
required this.i,
|
||||||
|
required this.currentValue,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final T i;
|
||||||
|
final T currentValue;
|
||||||
|
final Function(T) onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
var boxDecoration = BoxDecoration(
|
||||||
|
color: currentValue == i.toString()
|
||||||
|
? theme.colorScheme.primary
|
||||||
|
: theme.colorScheme.secondary,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
var decoratedBox = Container(
|
||||||
|
decoration: boxDecoration,
|
||||||
|
width: double.infinity,
|
||||||
|
height: 150,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
i.toString(),
|
||||||
|
style: theme.textTheme.labelLarge?.copyWith(
|
||||||
|
color: currentValue == i.toString()
|
||||||
|
? theme.colorScheme.onPrimary
|
||||||
|
: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => onTap(i),
|
||||||
|
child: decoratedBox,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,153 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_order_details/flutter_order_details.dart";
|
||||||
|
|
||||||
|
/// Order Detail input for a dropdown input.
|
||||||
|
class OrderDropdownInput<T> extends OrderDetailInput<T> {
|
||||||
|
/// Constructor for the order dropdown input.
|
||||||
|
OrderDropdownInput({
|
||||||
|
required super.title,
|
||||||
|
required super.outputKey,
|
||||||
|
required this.items,
|
||||||
|
super.titleStyle,
|
||||||
|
super.titleAlignment,
|
||||||
|
super.titlePadding,
|
||||||
|
super.subtitle,
|
||||||
|
super.errorIsRequired,
|
||||||
|
super.isRequired = true,
|
||||||
|
super.isReadOnly,
|
||||||
|
super.initialValue,
|
||||||
|
this.blurOnInteraction = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Items to show within the dropdown menu.
|
||||||
|
final List<T> items;
|
||||||
|
|
||||||
|
/// Whether or not the screen should blur when interacting.
|
||||||
|
final bool blurOnInteraction;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(
|
||||||
|
BuildContext context,
|
||||||
|
T? buildInitialValue,
|
||||||
|
Function({bool needsBlur}) onBlurBackground,
|
||||||
|
) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
void onItemChanged(T? value) {
|
||||||
|
currentValue = value;
|
||||||
|
onValueChanged?.call(value as T);
|
||||||
|
onBlurBackground(needsBlur: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onPopupOpen() {
|
||||||
|
if (blurOnInteraction)
|
||||||
|
onBlurBackground(
|
||||||
|
needsBlur: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var inputDecoration = InputDecoration(
|
||||||
|
labelText: titleStyle == OrderDetailTitleStyle.label ? title : null,
|
||||||
|
hintText: hint,
|
||||||
|
filled: true,
|
||||||
|
fillColor: theme.inputDecorationTheme.fillColor,
|
||||||
|
border: InputBorder.none,
|
||||||
|
);
|
||||||
|
|
||||||
|
currentValue =
|
||||||
|
currentValue ?? initialValue ?? buildInitialValue ?? items[0];
|
||||||
|
|
||||||
|
return buildOutline(
|
||||||
|
context,
|
||||||
|
DropdownButtonFormField<T>(
|
||||||
|
value: currentValue ?? initialValue ?? buildInitialValue ?? items[0],
|
||||||
|
selectedItemBuilder: (context) => items
|
||||||
|
.map(
|
||||||
|
(item) => Text(
|
||||||
|
item.toString(),
|
||||||
|
style: theme.textTheme.labelMedium,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
items: items
|
||||||
|
.map(
|
||||||
|
(item) => DropdownMenuItem<T>(
|
||||||
|
value: item,
|
||||||
|
child: _DropdownButtonBuilder<T>(
|
||||||
|
item: item,
|
||||||
|
currentValue: currentValue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onChanged: onItemChanged,
|
||||||
|
onTap: onPopupOpen,
|
||||||
|
style: theme.textTheme.labelMedium,
|
||||||
|
decoration: inputDecoration,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
icon: const Icon(Icons.keyboard_arrow_down_sharp),
|
||||||
|
validator: super.validate,
|
||||||
|
),
|
||||||
|
onBlurBackground,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DropdownButtonBuilder<T> extends StatelessWidget {
|
||||||
|
const _DropdownButtonBuilder({
|
||||||
|
required this.item,
|
||||||
|
this.currentValue,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final T item;
|
||||||
|
final T? currentValue;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
var textBuilder = Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Text(
|
||||||
|
item.toString(),
|
||||||
|
style: theme.textTheme.labelMedium?.copyWith(
|
||||||
|
color: item == currentValue ? theme.colorScheme.onPrimary : null,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
var selectedIcon = Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Icon(
|
||||||
|
Icons.check,
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: item == currentValue ? theme.colorScheme.primary : null,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
textBuilder,
|
||||||
|
if (currentValue == item) ...[
|
||||||
|
selectedIcon,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_order_details/flutter_order_details.dart";
|
||||||
|
|
||||||
|
/// Order Email input with predefined validators.
|
||||||
|
class OrderEmailInput extends OrderDetailInput<String> {
|
||||||
|
/// Constructor of the order email input.
|
||||||
|
OrderEmailInput({
|
||||||
|
required super.title,
|
||||||
|
required super.outputKey,
|
||||||
|
required this.textController,
|
||||||
|
super.titleStyle,
|
||||||
|
super.titleAlignment,
|
||||||
|
super.titlePadding,
|
||||||
|
super.subtitle,
|
||||||
|
super.hint,
|
||||||
|
super.errorIsRequired,
|
||||||
|
super.isRequired,
|
||||||
|
super.isReadOnly,
|
||||||
|
super.initialValue,
|
||||||
|
this.errorInvalidEmail = "Invalid email ( your_name@example.com )",
|
||||||
|
}) : super(
|
||||||
|
validators: [
|
||||||
|
(value) {
|
||||||
|
if (value != null && !RegExp(r"^\w+@\w+\.\w+$").hasMatch(value)) {
|
||||||
|
return errorInvalidEmail;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Text Controller for email input.
|
||||||
|
final TextEditingController textController;
|
||||||
|
|
||||||
|
/// Error message for invalid email.
|
||||||
|
final String errorInvalidEmail;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(
|
||||||
|
BuildContext context,
|
||||||
|
String? buildInitialValue,
|
||||||
|
Function({bool needsBlur}) onBlurBackground,
|
||||||
|
) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
textController.text = initialValue ?? buildInitialValue ?? "";
|
||||||
|
currentValue = textController.text;
|
||||||
|
|
||||||
|
return buildOutline(
|
||||||
|
context,
|
||||||
|
TextFormField(
|
||||||
|
style: theme.textTheme.labelMedium,
|
||||||
|
controller: textController,
|
||||||
|
onChanged: (String value) {
|
||||||
|
currentValue = value;
|
||||||
|
super.onValueChanged?.call(value);
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: titleStyle == OrderDetailTitleStyle.label ? title : null,
|
||||||
|
hintText: hint,
|
||||||
|
filled: true,
|
||||||
|
fillColor: theme.inputDecorationTheme.fillColor,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) => super.validate(value),
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
readOnly: isReadOnly,
|
||||||
|
),
|
||||||
|
onBlurBackground,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
173
packages/flutter_order_details/lib/src/models/order_input.dart
Normal file
173
packages/flutter_order_details/lib/src/models/order_input.dart
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_order_details/src/configuration/order_detail_title_style.dart";
|
||||||
|
|
||||||
|
/// Abstract class for order detail input.
|
||||||
|
/// Each input field must extend from this class.
|
||||||
|
abstract class OrderDetailInput<T> {
|
||||||
|
/// Constructor for the order detail input.
|
||||||
|
OrderDetailInput({
|
||||||
|
required this.title,
|
||||||
|
required this.outputKey,
|
||||||
|
this.titleStyle = OrderDetailTitleStyle.text,
|
||||||
|
this.titleAlignment = Alignment.centerLeft,
|
||||||
|
this.titlePadding = const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
this.subtitle,
|
||||||
|
this.isRequired = true,
|
||||||
|
this.isReadOnly = false,
|
||||||
|
this.initialValue,
|
||||||
|
this.validators = const [],
|
||||||
|
this.onValueChanged,
|
||||||
|
this.hint,
|
||||||
|
this.errorIsRequired = "This field is required",
|
||||||
|
this.paddingBetweenFields = const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Title of the input field.
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// Subtitle of the input field.
|
||||||
|
final String? subtitle;
|
||||||
|
|
||||||
|
/// The styling for the title.
|
||||||
|
final OrderDetailTitleStyle titleStyle;
|
||||||
|
|
||||||
|
/// The alignment of the titl
|
||||||
|
final Alignment titleAlignment;
|
||||||
|
|
||||||
|
/// Padding around the title.
|
||||||
|
final EdgeInsets titlePadding;
|
||||||
|
|
||||||
|
/// The output key of the input field.
|
||||||
|
final String outputKey;
|
||||||
|
|
||||||
|
/// Hint message of the input field.
|
||||||
|
final String? hint;
|
||||||
|
|
||||||
|
/// Determines if the input field is required.
|
||||||
|
final bool isRequired;
|
||||||
|
|
||||||
|
/// Error message for when an user does not insert something in the field
|
||||||
|
/// even though it is required.
|
||||||
|
final String errorIsRequired;
|
||||||
|
|
||||||
|
/// A read-only field that users cannot change.
|
||||||
|
final bool isReadOnly;
|
||||||
|
|
||||||
|
/// An initial value for the input field. This is ideal incombination
|
||||||
|
/// with the [isReadOnly] field.
|
||||||
|
final T? initialValue;
|
||||||
|
|
||||||
|
/// Internal current value. Do not use.
|
||||||
|
T? currentValue;
|
||||||
|
|
||||||
|
/// List of validators that should be executed when the input field
|
||||||
|
/// is validated.
|
||||||
|
List<String? Function(T?)> validators;
|
||||||
|
|
||||||
|
/// Function that is called when the value of the input field changes.
|
||||||
|
final Function(T)? onValueChanged;
|
||||||
|
|
||||||
|
/// Padding between the fields.
|
||||||
|
final EdgeInsets paddingBetweenFields;
|
||||||
|
|
||||||
|
/// Allows you to update the current value.
|
||||||
|
@protected
|
||||||
|
set updateValue(T value) {
|
||||||
|
currentValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Function that validates the input field. Automatically keeps track
|
||||||
|
/// of the [isRequired] keys and all the custom validators.
|
||||||
|
@protected
|
||||||
|
String? validate(T? value) {
|
||||||
|
if (isRequired && (value == null || value.toString().isEmpty)) {
|
||||||
|
return errorIsRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var validator in validators) {
|
||||||
|
var error = validator(value);
|
||||||
|
if (error != null) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the basic outline of an input field.
|
||||||
|
@protected
|
||||||
|
Widget buildOutline(
|
||||||
|
BuildContext context,
|
||||||
|
// ignore: avoid_annotating_with_dynamic
|
||||||
|
dynamic child,
|
||||||
|
Function({bool needsBlur}) onBlurBackground,
|
||||||
|
) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
if (titleStyle == OrderDetailTitleStyle.text) ...[
|
||||||
|
Align(
|
||||||
|
alignment: titleAlignment,
|
||||||
|
child: Padding(
|
||||||
|
padding: titlePadding,
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (subtitle != null) ...[
|
||||||
|
Padding(
|
||||||
|
padding: titlePadding,
|
||||||
|
child: Align(
|
||||||
|
alignment: titleAlignment,
|
||||||
|
child: Text(
|
||||||
|
subtitle!,
|
||||||
|
style: theme.textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
if (child is FormField || child is Widget) ...[
|
||||||
|
child,
|
||||||
|
] else if (child is List<FormField>) ...[
|
||||||
|
Column(
|
||||||
|
children: child
|
||||||
|
.map(
|
||||||
|
(FormField field) => Padding(
|
||||||
|
padding: paddingBetweenFields,
|
||||||
|
child: field,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
] else if (child is List<OrderDetailInput>) ...[
|
||||||
|
Column(
|
||||||
|
children: child
|
||||||
|
.map(
|
||||||
|
(OrderDetailInput input) => Padding(
|
||||||
|
padding: paddingBetweenFields,
|
||||||
|
child: input.build(
|
||||||
|
context,
|
||||||
|
input.initialValue,
|
||||||
|
onBlurBackground,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Abstract build function that each orderinput class must implement
|
||||||
|
/// themsleves. For a basic layout, they can use the [buildOutline] function.
|
||||||
|
Widget build(
|
||||||
|
BuildContext context,
|
||||||
|
T? buildInitialValue,
|
||||||
|
Function({bool needsBlur}) onBlurBackground,
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter/services.dart";
|
||||||
|
import "package:flutter_order_details/flutter_order_details.dart";
|
||||||
|
|
||||||
|
/// Order input for phone numbers with with predefined
|
||||||
|
/// text fields and validation.
|
||||||
|
class OrderPhoneInput extends OrderDetailInput<String> {
|
||||||
|
/// Constructor for the phone input.
|
||||||
|
OrderPhoneInput({
|
||||||
|
required super.title,
|
||||||
|
required super.outputKey,
|
||||||
|
required this.textController,
|
||||||
|
this.errorMustBe11Digits = "Number must be 11 digits (+31 6 XXXX XXXX)",
|
||||||
|
this.errorMustStartWith316 = "Number must start with +316",
|
||||||
|
this.errorMustBeNumeric = "Number must be numeric",
|
||||||
|
super.errorIsRequired,
|
||||||
|
super.subtitle,
|
||||||
|
super.titleAlignment,
|
||||||
|
super.titlePadding,
|
||||||
|
super.titleStyle,
|
||||||
|
super.isRequired,
|
||||||
|
super.isReadOnly,
|
||||||
|
super.initialValue,
|
||||||
|
}) : super(
|
||||||
|
validators: [
|
||||||
|
(value) {
|
||||||
|
if (value != null && value.length != 11) {
|
||||||
|
return errorMustBe11Digits;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
(value) {
|
||||||
|
if (value != null && !value.startsWith("316")) {
|
||||||
|
return errorMustStartWith316;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
(value) {
|
||||||
|
if (value != null && !RegExp(r"^\d+$").hasMatch(value)) {
|
||||||
|
return errorMustBeNumeric;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Text Controller for phone input.
|
||||||
|
final TextEditingController textController;
|
||||||
|
|
||||||
|
/// Error message that notifies the number must be 11 digits long.
|
||||||
|
final String errorMustBe11Digits;
|
||||||
|
|
||||||
|
/// Error message that notifies the number must start with +316
|
||||||
|
final String errorMustStartWith316;
|
||||||
|
|
||||||
|
/// Error message that notifies the number must be numeric.
|
||||||
|
final String errorMustBeNumeric;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(
|
||||||
|
BuildContext context,
|
||||||
|
String? buildInitialValue,
|
||||||
|
Function({bool needsBlur}) onBlurBackground,
|
||||||
|
) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
textController.text = initialValue ?? buildInitialValue ?? "31";
|
||||||
|
currentValue = textController.text;
|
||||||
|
|
||||||
|
return buildOutline(
|
||||||
|
context,
|
||||||
|
TextFormField(
|
||||||
|
style: theme.textTheme.labelMedium,
|
||||||
|
controller: textController,
|
||||||
|
onChanged: (String value) {
|
||||||
|
currentValue = value;
|
||||||
|
super.onValueChanged?.call(value);
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: titleStyle == OrderDetailTitleStyle.label ? title : null,
|
||||||
|
prefixText: "+",
|
||||||
|
prefixStyle: theme.textTheme.labelMedium,
|
||||||
|
hintText: hint,
|
||||||
|
filled: true,
|
||||||
|
fillColor: theme.inputDecorationTheme.fillColor,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) => super.validate(value),
|
||||||
|
readOnly: isReadOnly,
|
||||||
|
inputFormatters: [
|
||||||
|
FilteringTextInputFormatter.digitsOnly,
|
||||||
|
LengthLimitingTextInputFormatter(11), // international phone number
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onBlurBackground,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
/// OrderResult model.
|
||||||
|
/// When an user completes the field and presses the complete button,
|
||||||
|
/// the `onComplete` method returns an instance of this class that contains
|
||||||
|
/// all the developer-specified `outputKey`s and the value that was provided
|
||||||
|
/// by the user.
|
||||||
|
class OrderResult {
|
||||||
|
/// Constructor of the order result class.
|
||||||
|
OrderResult({
|
||||||
|
required this.order,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Map of `outputKey`s and their respected values.
|
||||||
|
final Map<String, dynamic> order;
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter/services.dart";
|
||||||
|
import "package:flutter_order_details/flutter_order_details.dart";
|
||||||
|
|
||||||
|
/// Default text input for order details.
|
||||||
|
class OrderTextInput extends OrderDetailInput<String> {
|
||||||
|
/// Default text input for order details.
|
||||||
|
OrderTextInput({
|
||||||
|
required super.title,
|
||||||
|
required super.outputKey,
|
||||||
|
required this.textController,
|
||||||
|
super.titleStyle,
|
||||||
|
super.titleAlignment,
|
||||||
|
super.titlePadding,
|
||||||
|
super.subtitle,
|
||||||
|
super.isRequired,
|
||||||
|
super.isReadOnly,
|
||||||
|
super.initialValue,
|
||||||
|
super.validators,
|
||||||
|
super.onValueChanged,
|
||||||
|
super.errorIsRequired,
|
||||||
|
super.hint,
|
||||||
|
this.inputFormatters = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Text Controller for the input field.
|
||||||
|
final TextEditingController textController;
|
||||||
|
|
||||||
|
/// List of input formatters for the text field.
|
||||||
|
final List<TextInputFormatter> inputFormatters;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(
|
||||||
|
BuildContext context,
|
||||||
|
String? buildInitialValue,
|
||||||
|
Function({bool needsBlur}) onBlurBackground,
|
||||||
|
) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
textController.text = initialValue ?? buildInitialValue ?? "";
|
||||||
|
currentValue = textController.text;
|
||||||
|
|
||||||
|
return buildOutline(
|
||||||
|
context,
|
||||||
|
TextFormField(
|
||||||
|
style: theme.textTheme.labelMedium,
|
||||||
|
controller: textController,
|
||||||
|
onChanged: (String value) {
|
||||||
|
currentValue = value;
|
||||||
|
super.onValueChanged?.call(value);
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: titleStyle == OrderDetailTitleStyle.label ? title : null,
|
||||||
|
hintText: hint,
|
||||||
|
filled: true,
|
||||||
|
fillColor: theme.inputDecorationTheme.fillColor,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: super.validate,
|
||||||
|
readOnly: isReadOnly,
|
||||||
|
inputFormatters: [
|
||||||
|
...inputFormatters,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onBlurBackground,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,353 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_order_details/flutter_order_details.dart";
|
||||||
|
import "package:flutter_order_details/src/models/formfield_error_builder.dart";
|
||||||
|
|
||||||
|
/// Order time picker input with predefined text fields and validation.
|
||||||
|
class OrderTimePicker extends OrderDetailInput<String> {
|
||||||
|
/// Constructor for the time picker.
|
||||||
|
OrderTimePicker({
|
||||||
|
required super.title,
|
||||||
|
required super.outputKey,
|
||||||
|
super.titleStyle,
|
||||||
|
super.titleAlignment,
|
||||||
|
super.titlePadding,
|
||||||
|
super.subtitle,
|
||||||
|
super.isRequired,
|
||||||
|
super.initialValue,
|
||||||
|
super.validators,
|
||||||
|
super.onValueChanged,
|
||||||
|
super.errorIsRequired,
|
||||||
|
super.hint,
|
||||||
|
this.beginTime = 9,
|
||||||
|
this.endTime = 17,
|
||||||
|
this.interval = 0.25,
|
||||||
|
this.morningLabel = "Morning",
|
||||||
|
this.afternoonLabel = "Afternoon",
|
||||||
|
this.eveningLabel = "Evening",
|
||||||
|
this.padding = const EdgeInsets.only(top: 12, bottom: 20.0),
|
||||||
|
}) : assert(
|
||||||
|
beginTime < endTime,
|
||||||
|
"Begin time cannot be greater than end time",
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Minimum time of times to show. For example 9 (for 9AM).
|
||||||
|
final double beginTime;
|
||||||
|
|
||||||
|
/// Final time to show. For example 17 (for 5PM).
|
||||||
|
final double endTime;
|
||||||
|
|
||||||
|
/// For each interval a button gets generated within the begin time and
|
||||||
|
/// the end time. For example 0.25 (for ever 15 minutes).
|
||||||
|
final double interval;
|
||||||
|
|
||||||
|
/// Translation for morning texts.
|
||||||
|
final String morningLabel;
|
||||||
|
|
||||||
|
/// Translation for afternoon texts.
|
||||||
|
final String afternoonLabel;
|
||||||
|
|
||||||
|
/// Translation for evening texts.
|
||||||
|
final String eveningLabel;
|
||||||
|
|
||||||
|
/// Padding around the time picker.
|
||||||
|
final EdgeInsets padding;
|
||||||
|
|
||||||
|
final _selectedTimeOfDay = _SelectedTimeOfDay();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(
|
||||||
|
BuildContext context,
|
||||||
|
String? buildInitialValue,
|
||||||
|
Function({bool needsBlur}) onBlurBackground,
|
||||||
|
) {
|
||||||
|
void updateSelectedTimeOfDay(_TimeOfDay timeOfDay) {
|
||||||
|
if (_selectedTimeOfDay.selectedTimeOfDay == timeOfDay) return;
|
||||||
|
_selectedTimeOfDay.selectedTimeOfDay = timeOfDay;
|
||||||
|
currentValue = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateSelectedTimeAsString(String? time) {
|
||||||
|
currentValue = time;
|
||||||
|
onValueChanged?.call(time ?? "");
|
||||||
|
_selectedTimeOfDay.selectedTime = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateSelectedTime(double time) {
|
||||||
|
if (currentValue == time.toString()) {
|
||||||
|
updateSelectedTimeAsString(null);
|
||||||
|
} else {
|
||||||
|
updateSelectedTimeAsString(time.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentValue != null) {
|
||||||
|
var currentValueAsDouble = double.parse(currentValue!);
|
||||||
|
for (var timeOfDay in _TimeOfDay.values) {
|
||||||
|
if (_isTimeWithinTimeOfDay(
|
||||||
|
currentValueAsDouble,
|
||||||
|
currentValueAsDouble,
|
||||||
|
timeOfDay,
|
||||||
|
)) {
|
||||||
|
_selectedTimeOfDay.selectedTimeOfDay = timeOfDay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateSelectedTimeAsString(currentValue);
|
||||||
|
} else {
|
||||||
|
for (var timeOfDay in _TimeOfDay.values) {
|
||||||
|
if (_isTimeWithinTimeOfDay(beginTime, endTime, timeOfDay)) {
|
||||||
|
_selectedTimeOfDay.selectedTimeOfDay = timeOfDay;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildOutline(
|
||||||
|
context,
|
||||||
|
ListenableBuilder(
|
||||||
|
listenable: _selectedTimeOfDay,
|
||||||
|
builder: (context, _) {
|
||||||
|
var startTime = _selectedTimeOfDay.selection != null
|
||||||
|
? _selectedTimeOfDay.selection!.minTime.clamp(beginTime, endTime)
|
||||||
|
: beginTime;
|
||||||
|
var finalTime = _selectedTimeOfDay.selection != null
|
||||||
|
? _selectedTimeOfDay.selection!.maxTime.clamp(beginTime, endTime)
|
||||||
|
: endTime;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_TimeOfDaySelector(
|
||||||
|
selectedTimeOfDay: _selectedTimeOfDay,
|
||||||
|
updateSelectedTimeOfDay: updateSelectedTimeOfDay,
|
||||||
|
startTime: beginTime,
|
||||||
|
endTime: endTime,
|
||||||
|
morningLabel: morningLabel,
|
||||||
|
afternoonLabel: afternoonLabel,
|
||||||
|
eveningLabel: eveningLabel,
|
||||||
|
padding: padding,
|
||||||
|
),
|
||||||
|
_TimeWrap<String>(
|
||||||
|
currentValue: currentValue ?? "",
|
||||||
|
startTime: startTime,
|
||||||
|
finalTime: finalTime,
|
||||||
|
interval: interval,
|
||||||
|
onTap: updateSelectedTime,
|
||||||
|
validate: super.validate,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
onBlurBackground,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isTimeWithinTimeOfDay(
|
||||||
|
double openingTime,
|
||||||
|
double closingTime,
|
||||||
|
_TimeOfDay timeOfDay,
|
||||||
|
) =>
|
||||||
|
(timeOfDay.minTime >= openingTime && timeOfDay.minTime <= closingTime) ||
|
||||||
|
(timeOfDay.maxTime > openingTime && timeOfDay.maxTime <= closingTime) ||
|
||||||
|
(timeOfDay.minTime <= openingTime && timeOfDay.maxTime >= closingTime);
|
||||||
|
|
||||||
|
class _TimeOfDaySelector extends StatelessWidget {
|
||||||
|
const _TimeOfDaySelector({
|
||||||
|
required this.selectedTimeOfDay,
|
||||||
|
required this.updateSelectedTimeOfDay,
|
||||||
|
required this.startTime,
|
||||||
|
required this.endTime,
|
||||||
|
required this.morningLabel,
|
||||||
|
required this.afternoonLabel,
|
||||||
|
required this.eveningLabel,
|
||||||
|
required this.padding,
|
||||||
|
});
|
||||||
|
|
||||||
|
final _SelectedTimeOfDay selectedTimeOfDay;
|
||||||
|
final Function(_TimeOfDay) updateSelectedTimeOfDay;
|
||||||
|
final double startTime;
|
||||||
|
final double endTime;
|
||||||
|
final String morningLabel;
|
||||||
|
final String afternoonLabel;
|
||||||
|
final String eveningLabel;
|
||||||
|
final EdgeInsets padding;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
String getLabelName(_TimeOfDay timeOfDay) => switch (timeOfDay) {
|
||||||
|
_TimeOfDay.morning => morningLabel,
|
||||||
|
_TimeOfDay.afternoon => afternoonLabel,
|
||||||
|
_TimeOfDay.evening => eveningLabel,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: padding,
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(90),
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
strokeAlign: BorderSide.strokeAlignOutside,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
for (var timeOfDay in _TimeOfDay.values) ...[
|
||||||
|
if (_isTimeWithinTimeOfDay(startTime, endTime, timeOfDay)) ...[
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => updateSelectedTimeOfDay(timeOfDay),
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: selectedTimeOfDay.selectedTimeOfDay == timeOfDay
|
||||||
|
? theme.colorScheme.primary
|
||||||
|
: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(90),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 20,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
getLabelName(timeOfDay),
|
||||||
|
style: theme.textTheme.labelMedium?.copyWith(
|
||||||
|
color:
|
||||||
|
selectedTimeOfDay.selectedTimeOfDay == timeOfDay
|
||||||
|
? Colors.white
|
||||||
|
: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimeWrap<T> extends FormField<T> {
|
||||||
|
_TimeWrap({
|
||||||
|
required this.currentValue,
|
||||||
|
required this.startTime,
|
||||||
|
required this.finalTime,
|
||||||
|
required this.interval,
|
||||||
|
required this.onTap,
|
||||||
|
required String? Function(T?) validate,
|
||||||
|
}) : super(
|
||||||
|
validator: (value) => validate(currentValue),
|
||||||
|
builder: (FormFieldState<T> field) => Column(
|
||||||
|
children: [
|
||||||
|
Wrap(
|
||||||
|
children: [
|
||||||
|
for (var i = startTime; i < finalTime; i += interval) ...[
|
||||||
|
_TimeWrapContent(
|
||||||
|
i: i,
|
||||||
|
currentValue: currentValue,
|
||||||
|
onTap: onTap,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (field.hasError) ...[
|
||||||
|
FormFieldErrorBuilder(errorMessage: field.errorText!),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final T currentValue;
|
||||||
|
final double startTime;
|
||||||
|
final double finalTime;
|
||||||
|
final double interval;
|
||||||
|
final Function(double) onTap;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimeWrapContent<T> extends StatelessWidget {
|
||||||
|
const _TimeWrapContent({
|
||||||
|
required this.i,
|
||||||
|
required this.currentValue,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double i;
|
||||||
|
final T currentValue;
|
||||||
|
final Function(double) onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
var boxDecoration = BoxDecoration(
|
||||||
|
color: currentValue == i.toString()
|
||||||
|
? theme.colorScheme.primary
|
||||||
|
: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
);
|
||||||
|
|
||||||
|
var decoratedBox = Container(
|
||||||
|
decoration: boxDecoration,
|
||||||
|
width: MediaQuery.of(context).size.width * .25,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 28,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'${i.floor().toString().padLeft(2, '0')}:'
|
||||||
|
'${((i - i.floor()) * 60).toInt().toString().padLeft(2, '0')}',
|
||||||
|
style: theme.textTheme.labelMedium?.copyWith(
|
||||||
|
color: currentValue == i.toString()
|
||||||
|
? Colors.white
|
||||||
|
: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => onTap(i),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(4.0),
|
||||||
|
child: decoratedBox,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SelectedTimeOfDay extends ChangeNotifier {
|
||||||
|
_TimeOfDay? selection;
|
||||||
|
String? time = "";
|
||||||
|
|
||||||
|
_TimeOfDay? get selectedTimeOfDay => selection;
|
||||||
|
String? get selectedTime => time;
|
||||||
|
|
||||||
|
set selectedTimeOfDay(_TimeOfDay? value) {
|
||||||
|
selection = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
set selectedTime(String? value) {
|
||||||
|
time = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _TimeOfDay {
|
||||||
|
morning(0, 12),
|
||||||
|
afternoon(12, 18),
|
||||||
|
evening(18, 24);
|
||||||
|
|
||||||
|
const _TimeOfDay(this.minTime, this.maxTime);
|
||||||
|
|
||||||
|
final double minTime;
|
||||||
|
final double maxTime;
|
||||||
|
}
|
|
@ -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",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
|
@ -0,0 +1,273 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_order_details/flutter_order_details.dart";
|
||||||
|
|
||||||
|
/// Order Detail Screen.
|
||||||
|
class OrderDetailScreen extends StatefulWidget {
|
||||||
|
/// Screen that builds all forms based on the configuration.
|
||||||
|
const OrderDetailScreen({
|
||||||
|
required this.configuration,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Configuration for the screen.
|
||||||
|
final OrderDetailConfiguration configuration;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<OrderDetailScreen> createState() => _OrderDetailScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OrderDetailScreenState extends State<OrderDetailScreen> {
|
||||||
|
final _CurrentStep _currentStep = _CurrentStep();
|
||||||
|
|
||||||
|
final OrderResult _orderResult = OrderResult(order: {});
|
||||||
|
|
||||||
|
bool _blurBackground = false;
|
||||||
|
|
||||||
|
void _toggleBlurBackground({bool? needsBlur}) {
|
||||||
|
setState(() {
|
||||||
|
_blurBackground = needsBlur!;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
var pageBody = SafeArea(
|
||||||
|
left: false,
|
||||||
|
right: false,
|
||||||
|
bottom: true,
|
||||||
|
child: _OrderDetailBody(
|
||||||
|
configuration: widget.configuration,
|
||||||
|
orderResult: _orderResult,
|
||||||
|
currentStep: _currentStep,
|
||||||
|
onBlurBackground: _toggleBlurBackground,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
var pageBlur = GestureDetector(
|
||||||
|
onTap: () => _toggleBlurBackground(needsBlur: false),
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surface.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: widget.configuration.appBar,
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
pageBody,
|
||||||
|
if (_blurBackground) pageBlur,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CurrentStep extends ChangeNotifier {
|
||||||
|
int _step = 0;
|
||||||
|
|
||||||
|
int get step => _step;
|
||||||
|
|
||||||
|
void increment() {
|
||||||
|
_step++;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void decrement() {
|
||||||
|
_step--;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OrderDetailBody extends StatelessWidget {
|
||||||
|
const _OrderDetailBody({
|
||||||
|
required this.configuration,
|
||||||
|
required this.orderResult,
|
||||||
|
required this.currentStep,
|
||||||
|
required this.onBlurBackground,
|
||||||
|
});
|
||||||
|
|
||||||
|
final OrderDetailConfiguration configuration;
|
||||||
|
final OrderResult orderResult;
|
||||||
|
final _CurrentStep currentStep;
|
||||||
|
final Function({bool needsBlur}) onBlurBackground;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => ListenableBuilder(
|
||||||
|
listenable: currentStep,
|
||||||
|
builder: (context, _) => Builder(
|
||||||
|
builder: (context) => _FormBuilder(
|
||||||
|
currentStep: currentStep,
|
||||||
|
orderResult: orderResult,
|
||||||
|
configuration: configuration,
|
||||||
|
onBlurBackground: onBlurBackground,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FormBuilder extends StatelessWidget {
|
||||||
|
const _FormBuilder({
|
||||||
|
required this.currentStep,
|
||||||
|
required this.configuration,
|
||||||
|
required this.orderResult,
|
||||||
|
required this.onBlurBackground,
|
||||||
|
});
|
||||||
|
|
||||||
|
final _CurrentStep currentStep;
|
||||||
|
final OrderDetailConfiguration configuration;
|
||||||
|
final OrderResult orderResult;
|
||||||
|
|
||||||
|
final Function({bool needsBlur}) onBlurBackground;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
var progressIndicator = LinearProgressIndicator(
|
||||||
|
value: currentStep.step / configuration.steps.length,
|
||||||
|
backgroundColor: theme.colorScheme.surface,
|
||||||
|
);
|
||||||
|
|
||||||
|
var stepForm = Form(
|
||||||
|
key: configuration.steps[currentStep.step].formKey,
|
||||||
|
child: _StepBuilder(
|
||||||
|
configuration: configuration,
|
||||||
|
currentStep: configuration.steps[currentStep.step],
|
||||||
|
orderResult: orderResult,
|
||||||
|
theme: theme,
|
||||||
|
onBlurBackground: onBlurBackground,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
void onPressedNext() {
|
||||||
|
var formInfo = configuration.steps[currentStep.step];
|
||||||
|
var formkey = formInfo.formKey;
|
||||||
|
for (var input in formInfo.fields) {
|
||||||
|
orderResult.order[input.outputKey] = input.currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formkey.currentState!.validate()) {
|
||||||
|
currentStep.increment();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onPressedPrevious() {
|
||||||
|
var formInfo = configuration.steps[currentStep.step];
|
||||||
|
for (var input in formInfo.fields) {
|
||||||
|
orderResult.order[input.outputKey] = input.currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStep.decrement();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onPressedComplete() {
|
||||||
|
var formInfo = configuration.steps[currentStep.step];
|
||||||
|
var formkey = formInfo.formKey;
|
||||||
|
for (var input in formInfo.fields) {
|
||||||
|
orderResult.order[input.outputKey] = input.currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formkey.currentState!.validate()) {
|
||||||
|
configuration.onCompleted(orderResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var navigationControl = Row(
|
||||||
|
children: [
|
||||||
|
if (currentStep.step > 0) ...[
|
||||||
|
TextButton(
|
||||||
|
onPressed: onPressedPrevious,
|
||||||
|
child: Text(
|
||||||
|
configuration.localization.backButton,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const Spacer(),
|
||||||
|
if (currentStep.step < configuration.steps.length - 1) ...[
|
||||||
|
TextButton(
|
||||||
|
onPressed: onPressedNext,
|
||||||
|
child: Text(
|
||||||
|
configuration.localization.nextButton,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
TextButton(
|
||||||
|
onPressed: onPressedComplete,
|
||||||
|
child: Text(
|
||||||
|
configuration.localization.completeButton,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
if (configuration.progressIndicator) ...[
|
||||||
|
progressIndicator,
|
||||||
|
],
|
||||||
|
stepForm,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: navigationControl,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StepBuilder extends StatelessWidget {
|
||||||
|
const _StepBuilder({
|
||||||
|
required this.configuration,
|
||||||
|
required this.currentStep,
|
||||||
|
required this.orderResult,
|
||||||
|
required this.theme,
|
||||||
|
required this.onBlurBackground,
|
||||||
|
});
|
||||||
|
|
||||||
|
final OrderDetailConfiguration configuration;
|
||||||
|
final OrderDetailStep currentStep;
|
||||||
|
final OrderResult orderResult;
|
||||||
|
final ThemeData theme;
|
||||||
|
final Function({bool needsBlur}) onBlurBackground;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var title = currentStep.stepName != null
|
||||||
|
? Padding(
|
||||||
|
padding: configuration.titlePadding,
|
||||||
|
child: Text(
|
||||||
|
currentStep.stepName!,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
title,
|
||||||
|
for (var input in currentStep.fields)
|
||||||
|
Padding(
|
||||||
|
padding: configuration.inputFieldPadding,
|
||||||
|
child: input.build(
|
||||||
|
context,
|
||||||
|
orderResult.order[input.outputKey],
|
||||||
|
onBlurBackground,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,28 +1,13 @@
|
||||||
name: flutter_order_details
|
name: flutter_order_details
|
||||||
description: "A Flutter module for order details."
|
description: "A Flutter module for order details."
|
||||||
version: 2.0.0
|
version: 1.0.0
|
||||||
publish_to: "none"
|
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.3.0 <4.0.0"
|
sdk: '>=3.3.0 <4.0.0'
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: 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:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@ -33,3 +18,13 @@ dev_dependencies:
|
||||||
ref: 7.0.0
|
ref: 7.0.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
|
# assets:
|
||||||
|
# - images/a_dot_burr.jpeg
|
||||||
|
# - images/a_dot_ham.jpeg
|
||||||
|
|
||||||
|
# fonts:
|
||||||
|
# - family: Schyler
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/Schyler-Regular.ttf
|
||||||
|
# - asset: fonts/Schyler-Italic.ttf
|
||||||
|
# style: italic
|
||||||
|
|
|
@ -2,7 +2,12 @@
|
||||||
/// detailed view of each product.
|
/// detailed view of each product.
|
||||||
library flutter_product_page;
|
library flutter_product_page;
|
||||||
|
|
||||||
|
export "src/configuration/product_page_category_styling_configuration.dart";
|
||||||
export "src/configuration/product_page_configuration.dart";
|
export "src/configuration/product_page_configuration.dart";
|
||||||
|
export "src/configuration/product_page_content.dart";
|
||||||
|
export "src/configuration/product_page_localization.dart";
|
||||||
export "src/configuration/product_page_shop_selector_style.dart";
|
export "src/configuration/product_page_shop_selector_style.dart";
|
||||||
export "src/configuration/product_page_translations.dart";
|
export "src/models/product.dart";
|
||||||
export "src/product_page_screen.dart";
|
export "src/models/product_page_shop.dart";
|
||||||
|
export "src/ui/product_page.dart";
|
||||||
|
export "src/ui/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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_nested_categories/flutter_nested_categories.dart"
|
||||||
|
show CategoryHeaderStyling;
|
||||||
|
|
||||||
|
/// Configuration for the styling of the category list on the product page.
|
||||||
|
/// This configuration allows to customize the title, header styling and
|
||||||
|
/// the collapsible behavior of the categories.
|
||||||
|
class ProductPageCategoryStylingConfiguration {
|
||||||
|
/// Constructor to create a new instance of
|
||||||
|
/// [ProductPageCategoryStylingConfiguration].
|
||||||
|
const ProductPageCategoryStylingConfiguration({
|
||||||
|
this.headerStyling,
|
||||||
|
this.headerCentered = false,
|
||||||
|
this.customTitle,
|
||||||
|
this.title,
|
||||||
|
this.titleStyle,
|
||||||
|
this.titleCentered = false,
|
||||||
|
this.isCategoryCollapsible = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Optional title for the category list. This will be displayed at the
|
||||||
|
/// top of the list.
|
||||||
|
final String? title;
|
||||||
|
|
||||||
|
/// Optional custom title widget for the category list. This will be
|
||||||
|
/// displayed at the top of the list. If set, the text title will be
|
||||||
|
/// ignored.
|
||||||
|
final Widget? customTitle;
|
||||||
|
|
||||||
|
/// Optional title style for the title of the category list. This will
|
||||||
|
/// be applied to the title of the category list. If not set, the default
|
||||||
|
/// text style will be used.
|
||||||
|
final TextStyle? titleStyle;
|
||||||
|
|
||||||
|
/// Configure if the title should be centered.
|
||||||
|
///
|
||||||
|
/// Default is false.
|
||||||
|
final bool titleCentered;
|
||||||
|
|
||||||
|
/// Optional header styling for the categories. This will be applied to
|
||||||
|
/// the name of the categories. If not set, the default text style will
|
||||||
|
/// be used.
|
||||||
|
final CategoryHeaderStyling? headerStyling;
|
||||||
|
|
||||||
|
/// Configure if the category header should be centered.
|
||||||
|
///
|
||||||
|
/// Default is false.
|
||||||
|
final bool headerCentered;
|
||||||
|
|
||||||
|
/// Configure if the category should be collapsible.
|
||||||
|
///
|
||||||
|
/// Default is true.
|
||||||
|
final bool isCategoryCollapsible;
|
||||||
|
}
|
|
@ -1,96 +1,185 @@
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_product_page/flutter_product_page.dart";
|
import "package:flutter_product_page/flutter_product_page.dart";
|
||||||
import "package:flutter_product_page/src/widgets/product_item_popup.dart";
|
import "package:flutter_product_page/src/ui/widgets/product_item_popup.dart";
|
||||||
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
|
|
||||||
|
|
||||||
/// Configuration for the product page.
|
/// Configuration for the product page.
|
||||||
class ProductPageConfiguration {
|
class ProductPageConfiguration {
|
||||||
/// Constructor for the product page configuration.
|
/// Constructor for the product page configuration.
|
||||||
ProductPageConfiguration({
|
ProductPageConfiguration({
|
||||||
required this.shoppingService,
|
|
||||||
required this.shops,
|
required this.shops,
|
||||||
|
//
|
||||||
required this.getProducts,
|
required this.getProducts,
|
||||||
|
//
|
||||||
required this.onAddToCart,
|
required this.onAddToCart,
|
||||||
required this.onNavigateToShoppingCart,
|
required this.onNavigateToShoppingCart,
|
||||||
required this.getProductsInShoppingCart,
|
this.navigateToShoppingCartBuilder,
|
||||||
this.shoppingCartButtonBuilder,
|
//
|
||||||
this.initialShopId,
|
this.initialShopId,
|
||||||
|
//
|
||||||
this.productBuilder,
|
this.productBuilder,
|
||||||
|
//
|
||||||
this.onShopSelectionChange,
|
this.onShopSelectionChange,
|
||||||
this.translations = const ProductPageTranslations(),
|
this.getProductsInShoppingCart,
|
||||||
this.shopSelectorStyle = ShopSelectorStyle.row,
|
//
|
||||||
|
this.localizations = const ProductPageLocalization(),
|
||||||
|
//
|
||||||
|
this.shopSelectorStyle = ShopSelectorStyle.spacedWrap,
|
||||||
|
this.categoryStylingConfiguration =
|
||||||
|
const ProductPageCategoryStylingConfiguration(),
|
||||||
|
//
|
||||||
this.pagePadding = const EdgeInsets.all(4),
|
this.pagePadding = const EdgeInsets.all(4),
|
||||||
this.appBarBuilder,
|
//
|
||||||
|
this.appBar,
|
||||||
this.bottomNavigationBar,
|
this.bottomNavigationBar,
|
||||||
this.onProductDetail,
|
//
|
||||||
this.discountDescription,
|
Function(
|
||||||
this.noContentBuilder,
|
BuildContext context,
|
||||||
this.errorBuilder,
|
ProductPageProduct product,
|
||||||
this.shopselectorBuilder,
|
)? onProductDetail,
|
||||||
this.discountBuilder,
|
String Function(
|
||||||
this.categoryListBuilder,
|
ProductPageProduct product,
|
||||||
this.selectedCategoryBuilder,
|
)? getDiscountDescription,
|
||||||
|
Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
ProductPageProduct product,
|
||||||
|
)? productPopupBuilder,
|
||||||
|
Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
)? noContentBuilder,
|
||||||
|
Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
Object? error,
|
||||||
|
StackTrace? stackTrace,
|
||||||
|
)? errorBuilder,
|
||||||
}) {
|
}) {
|
||||||
onProductDetail ??= _onProductDetail;
|
_productPopupBuilder = productPopupBuilder;
|
||||||
discountDescription ??= _defaultDiscountDescription;
|
_productPopupBuilder ??=
|
||||||
}
|
(BuildContext context, ProductPageProduct product) => ProductItemPopup(
|
||||||
|
product: product,
|
||||||
|
configuration: this,
|
||||||
|
);
|
||||||
|
|
||||||
/// The shopping service that is used
|
_onProductDetail = onProductDetail;
|
||||||
final ShoppingService shoppingService;
|
_onProductDetail ??=
|
||||||
|
(BuildContext context, ProductPageProduct product) async {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
await showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
backgroundColor: theme.colorScheme.surface,
|
||||||
|
builder: (context) => _productPopupBuilder!(
|
||||||
|
context,
|
||||||
|
product,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
_noContentBuilder = noContentBuilder;
|
||||||
|
_noContentBuilder ??= (BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
"No content",
|
||||||
|
style: theme.textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
_errorBuilder = errorBuilder;
|
||||||
|
_errorBuilder ??=
|
||||||
|
(BuildContext context, Object? error, StackTrace? stackTrace) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
"Error: $error",
|
||||||
|
style: theme.textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
_getDiscountDescription = getDiscountDescription;
|
||||||
|
_getDiscountDescription ??=
|
||||||
|
(ProductPageProduct product) => "${product.name} is on sale!";
|
||||||
|
}
|
||||||
|
|
||||||
/// The shop that is initially selected.
|
/// The shop that is initially selected.
|
||||||
final String? initialShopId;
|
final String? initialShopId;
|
||||||
|
|
||||||
/// A list of all the shops that the user must be able to navigate from.
|
/// A list of all the shops that the user must be able to navigate from.
|
||||||
final Future<List<Shop>> Function() shops;
|
final Future<List<ProductPageShop>> shops;
|
||||||
|
|
||||||
/// A function that returns all the products that belong to a certain shop.
|
/// A function that returns all the products that belong to a certain shop.
|
||||||
/// The function must return a [List<Product>].
|
/// The function must return a [ProductPageContent] object.
|
||||||
final Future<List<Product>> Function(Shop shop) getProducts;
|
final Future<ProductPageContent> Function(ProductPageShop shop) getProducts;
|
||||||
|
|
||||||
/// The localizations for the product page.
|
/// The localizations for the product page.
|
||||||
final ProductPageTranslations translations;
|
final ProductPageLocalization localizations;
|
||||||
|
|
||||||
/// Builder for the product item. These items will be displayed in the list
|
/// Builder for the product item. These items will be displayed in the list
|
||||||
/// for each product in their seperated category. This builder should only
|
/// for each product in their seperated category. This builder should only
|
||||||
/// build the widget for one specific product. This builder has a default
|
/// build the widget for one specific product. This builder has a default
|
||||||
/// in-case the developer does not override it.
|
/// in-case the developer does not override it.
|
||||||
final Widget Function(
|
Widget Function(BuildContext context, ProductPageProduct product)?
|
||||||
BuildContext context,
|
productBuilder;
|
||||||
Product product,
|
|
||||||
ProductPageConfiguration configuration,
|
|
||||||
)? productBuilder;
|
|
||||||
|
|
||||||
/// The builder for the product popup. This builder should return a widget
|
late Widget Function(BuildContext context, ProductPageProduct product)?
|
||||||
Function(
|
_productPopupBuilder;
|
||||||
BuildContext context,
|
|
||||||
Product product,
|
/// The builder for the product popup. This popup will be displayed when the
|
||||||
String closeText,
|
/// user clicks on a product. This builder should only build the widget that
|
||||||
)? onProductDetail;
|
/// displays the content of one specific product.
|
||||||
|
/// This builder has a default in-case the developer
|
||||||
|
Widget Function(BuildContext context, ProductPageProduct product)
|
||||||
|
get productPopupBuilder => _productPopupBuilder!;
|
||||||
|
|
||||||
|
late Function(BuildContext context, ProductPageProduct product)?
|
||||||
|
_onProductDetail;
|
||||||
|
|
||||||
|
/// This function handles the creation of the product detail popup. This
|
||||||
|
/// function has a default in-case the developer does not override it.
|
||||||
|
/// The default intraction is a popup, but this can be overriden.
|
||||||
|
Function(BuildContext context, ProductPageProduct product)
|
||||||
|
get onProductDetail => _onProductDetail!;
|
||||||
|
|
||||||
|
late Widget Function(BuildContext context)? _noContentBuilder;
|
||||||
|
|
||||||
|
/// The no content builder is used when a shop has no products. This builder
|
||||||
|
/// has a default in-case the developer does not override it.
|
||||||
|
Function(BuildContext context)? get noContentBuilder => _noContentBuilder;
|
||||||
|
|
||||||
/// The builder for the shopping cart. This builder should return a widget
|
/// The builder for the shopping cart. This builder should return a widget
|
||||||
/// that navigates to the shopping cart overview page.
|
/// that navigates to the shopping cart overview page.
|
||||||
final Widget Function(
|
Widget Function(BuildContext context)? navigateToShoppingCartBuilder;
|
||||||
BuildContext context,
|
|
||||||
ProductPageConfiguration configuration,
|
|
||||||
)? shoppingCartButtonBuilder;
|
|
||||||
|
|
||||||
/// The function that returns the discount description for a product.
|
late Widget Function(
|
||||||
String Function(
|
BuildContext context,
|
||||||
Product product,
|
Object? error,
|
||||||
)? discountDescription;
|
StackTrace? stackTrace,
|
||||||
|
)? _errorBuilder;
|
||||||
|
|
||||||
|
/// The error builder is used when an error occurs. This builder has a default
|
||||||
|
/// in-case the developer does not override it.
|
||||||
|
Widget Function(BuildContext context, Object? error, StackTrace? stackTrace)?
|
||||||
|
get errorBuilder => _errorBuilder;
|
||||||
|
|
||||||
|
late String Function(ProductPageProduct product)? _getDiscountDescription;
|
||||||
|
|
||||||
|
/// The function that returns the description of the discount for a product.
|
||||||
|
/// This allows you to translate and give custom messages for each product.
|
||||||
|
String Function(ProductPageProduct product)? get getDiscountDescription =>
|
||||||
|
_getDiscountDescription!;
|
||||||
|
|
||||||
/// This function must be implemented by the developer and should handle the
|
/// This function must be implemented by the developer and should handle the
|
||||||
/// adding of a product to the cart.
|
/// adding of a product to the cart.
|
||||||
Function(Product product) onAddToCart;
|
Function(ProductPageProduct product) onAddToCart;
|
||||||
|
|
||||||
/// This function gets executed when the user changes the shop selection.
|
/// This function gets executed when the user changes the shop selection.
|
||||||
/// This function always fires upon first load with the initial shop as well.
|
/// This function always fires upon first load with the initial shop as well.
|
||||||
final Function(Shop shop)? onShopSelectionChange;
|
final Function(ProductPageShop shop)? onShopSelectionChange;
|
||||||
|
|
||||||
/// This function must be implemented by the developer and should handle the
|
/// This function must be implemented by the developer and should handle the
|
||||||
/// navigation to the shopping cart overview page.
|
/// navigation to the shopping cart overview page.
|
||||||
final int Function() getProductsInShoppingCart;
|
final int Function()? getProductsInShoppingCart;
|
||||||
|
|
||||||
/// This function must be implemented by the developer and should handle the
|
/// This function must be implemented by the developer and should handle the
|
||||||
/// navigation to the shopping cart overview page.
|
/// navigation to the shopping cart overview page.
|
||||||
|
@ -99,6 +188,9 @@ class ProductPageConfiguration {
|
||||||
/// The style of the shop selector.
|
/// The style of the shop selector.
|
||||||
final ShopSelectorStyle shopSelectorStyle;
|
final ShopSelectorStyle shopSelectorStyle;
|
||||||
|
|
||||||
|
/// The styling configuration for the category list.
|
||||||
|
final ProductPageCategoryStylingConfiguration categoryStylingConfiguration;
|
||||||
|
|
||||||
/// The padding for the page.
|
/// The padding for the page.
|
||||||
final EdgeInsets pagePadding;
|
final EdgeInsets pagePadding;
|
||||||
|
|
||||||
|
@ -106,68 +198,5 @@ class ProductPageConfiguration {
|
||||||
final Widget? bottomNavigationBar;
|
final Widget? bottomNavigationBar;
|
||||||
|
|
||||||
/// Optional app bar that you can pass to the order detail screen.
|
/// Optional app bar that you can pass to the order detail screen.
|
||||||
final PreferredSizeWidget Function(BuildContext context)? appBarBuilder;
|
final PreferredSizeWidget? appBar;
|
||||||
|
|
||||||
/// 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";
|
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import "package:flutter_product_page/flutter_product_page.dart";
|
||||||
|
|
||||||
|
/// Return type that contains the products and an optional discounted product.
|
||||||
|
class ProductPageContent {
|
||||||
|
/// Default constructor for this class.
|
||||||
|
const ProductPageContent({
|
||||||
|
required this.products,
|
||||||
|
this.discountedProduct,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// List of products that belong to the shop.
|
||||||
|
final List<ProductPageProduct> products;
|
||||||
|
|
||||||
|
/// Optional highlighted discounted product to display.
|
||||||
|
final ProductPageProduct? discountedProduct;
|
||||||
|
}
|
|
@ -1,13 +1,11 @@
|
||||||
/// Localization for the product page
|
/// Localization for the product page
|
||||||
class ProductPageTranslations {
|
class ProductPageLocalization {
|
||||||
/// Default constructor
|
/// Default constructor
|
||||||
const ProductPageTranslations({
|
const ProductPageLocalization({
|
||||||
this.navigateToShoppingCart = "View shopping cart",
|
this.navigateToShoppingCart = "To shopping cart",
|
||||||
this.discountTitle = "Weekly offer",
|
this.discountTitle = "Discount",
|
||||||
this.failedToLoadImageExplenation = "Failed to load image",
|
this.failedToLoadImageExplenation = "Failed to load image",
|
||||||
this.close = "Close",
|
this.close = "Close",
|
||||||
this.categoryItemListTitle = "What would you like to order",
|
|
||||||
this.appBarTitle = "ProductPage",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Message to navigate to the shopping cart
|
/// Message to navigate to the shopping cart
|
||||||
|
@ -21,10 +19,4 @@ class ProductPageTranslations {
|
||||||
|
|
||||||
/// Close button for the product page
|
/// Close button for the product page
|
||||||
final String close;
|
final String close;
|
||||||
|
|
||||||
/// Title for the category item list
|
|
||||||
final String categoryItemListTitle;
|
|
||||||
|
|
||||||
/// Title for the app bar
|
|
||||||
final String appBarTitle;
|
|
||||||
}
|
}
|
26
packages/flutter_product_page/lib/src/models/product.dart
Normal file
26
packages/flutter_product_page/lib/src/models/product.dart
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
/// The product page shop class contains all the required information
|
||||||
|
///
|
||||||
|
/// This is a mixin class because another package will implement it, and the
|
||||||
|
/// 'MyProduct' class might have to extend another class as well.
|
||||||
|
mixin ProductPageProduct {
|
||||||
|
/// The unique identifier for the product.
|
||||||
|
String get id;
|
||||||
|
|
||||||
|
/// The name of the product.
|
||||||
|
String get name;
|
||||||
|
|
||||||
|
/// The image URL of the product.
|
||||||
|
String get imageUrl;
|
||||||
|
|
||||||
|
/// The category of the product.
|
||||||
|
String get category;
|
||||||
|
|
||||||
|
/// The price of the product.
|
||||||
|
double get price;
|
||||||
|
|
||||||
|
/// Whether the product has a discount or not.
|
||||||
|
bool get hasDiscount;
|
||||||
|
|
||||||
|
/// The discounted price of the product. Only used if [hasDiscount] is true.
|
||||||
|
double? get discountPrice;
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
/// The product page shop class contains all the required information
|
||||||
|
/// that needs to be known about a certain shop.
|
||||||
|
///
|
||||||
|
/// In your own implemententation, you must extend from this class so you can
|
||||||
|
/// add more fields to this class to suit your needs.
|
||||||
|
class ProductPageShop {
|
||||||
|
/// The default constructor for this class.
|
||||||
|
const ProductPageShop({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The unique identifier for the shop.
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
/// The name of the shop.
|
||||||
|
final String name;
|
||||||
|
}
|
|
@ -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,18 +1,31 @@
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_nested_categories/flutter_nested_categories.dart";
|
import "package:flutter_nested_categories/flutter_nested_categories.dart";
|
||||||
import "package:flutter_product_page/flutter_product_page.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_product_page/src/services/shopping_cart_notifier.dart";
|
||||||
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
|
import "package:flutter_product_page/src/ui/components/product_item.dart";
|
||||||
|
|
||||||
|
/// A function that is called when a product is added to the cart.
|
||||||
|
ProductPageProduct onAddToCartWrapper(
|
||||||
|
ProductPageConfiguration configuration,
|
||||||
|
ShoppingCartNotifier shoppingCartNotifier,
|
||||||
|
ProductPageProduct product,
|
||||||
|
) {
|
||||||
|
shoppingCartNotifier.productsChanged();
|
||||||
|
|
||||||
|
configuration.onAddToCart(product);
|
||||||
|
|
||||||
|
return product;
|
||||||
|
}
|
||||||
|
|
||||||
/// Generates a [CategoryList] from a list of [Product]s and a
|
/// Generates a [CategoryList] from a list of [Product]s and a
|
||||||
/// [ProductPageConfiguration].
|
/// [ProductPageConfiguration].
|
||||||
Widget getCategoryList(
|
CategoryList getCategoryList(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
ProductPageConfiguration configuration,
|
ProductPageConfiguration configuration,
|
||||||
List<Product> products,
|
ShoppingCartNotifier shoppingCartNotifier,
|
||||||
|
List<ProductPageProduct> products,
|
||||||
) {
|
) {
|
||||||
var theme = Theme.of(context);
|
var categorizedProducts = <String, List<ProductPageProduct>>{};
|
||||||
var categorizedProducts = <String, List<Product>>{};
|
|
||||||
for (var product in products) {
|
for (var product in products) {
|
||||||
if (!categorizedProducts.containsKey(product.category)) {
|
if (!categorizedProducts.containsKey(product.category)) {
|
||||||
categorizedProducts[product.category] = [];
|
categorizedProducts[product.category] = [];
|
||||||
|
@ -25,14 +38,18 @@ Widget getCategoryList(
|
||||||
categorizedProducts.forEach((categoryName, productList) {
|
categorizedProducts.forEach((categoryName, productList) {
|
||||||
var productWidgets = productList
|
var productWidgets = productList
|
||||||
.map(
|
.map(
|
||||||
(product) =>
|
(product) => configuration.productBuilder != null
|
||||||
configuration.productBuilder
|
? configuration.productBuilder!(context, product)
|
||||||
?.call(context, product, configuration) ??
|
: ProductItem(
|
||||||
DefaultProductItem(
|
|
||||||
product: product,
|
product: product,
|
||||||
onAddToCart: configuration.onAddToCart,
|
onProductDetail: configuration.onProductDetail,
|
||||||
onProductDetail: configuration.onProductDetail!,
|
onAddToCart: (ProductPageProduct product) =>
|
||||||
translations: configuration.translations,
|
onAddToCartWrapper(
|
||||||
|
configuration,
|
||||||
|
shoppingCartNotifier,
|
||||||
|
product,
|
||||||
|
),
|
||||||
|
localizations: configuration.localizations,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
@ -42,19 +59,15 @@ Widget getCategoryList(
|
||||||
);
|
);
|
||||||
categories.add(category);
|
categories.add(category);
|
||||||
});
|
});
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
return CategoryList(
|
||||||
children: [
|
title: configuration.categoryStylingConfiguration.title,
|
||||||
for (var category in categories) ...[
|
titleStyle: configuration.categoryStylingConfiguration.titleStyle,
|
||||||
Text(
|
customTitle: configuration.categoryStylingConfiguration.customTitle,
|
||||||
category.name!,
|
headerCentered: configuration.categoryStylingConfiguration.headerCentered,
|
||||||
style: theme.textTheme.titleMedium,
|
headerStyling: configuration.categoryStylingConfiguration.headerStyling,
|
||||||
),
|
isCategoryCollapsible:
|
||||||
Column(
|
configuration.categoryStylingConfiguration.isCategoryCollapsible,
|
||||||
children: category.content,
|
content: categories,
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_product_page/src/models/product_page_shop.dart";
|
||||||
|
|
||||||
|
/// A service that provides the currently selected shop.
|
||||||
|
class SelectedShopService extends ChangeNotifier {
|
||||||
|
/// Creates a [SelectedShopService].
|
||||||
|
SelectedShopService();
|
||||||
|
|
||||||
|
ProductPageShop? _selectedShop;
|
||||||
|
|
||||||
|
/// Updates the selected shop.
|
||||||
|
void selectShop(ProductPageShop shop) {
|
||||||
|
if (_selectedShop == shop) return;
|
||||||
|
|
||||||
|
_selectedShop = shop;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The currently selected shop.
|
||||||
|
ProductPageShop? get selectedShop => _selectedShop;
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
|
/// Class that notifies listeners when the products in the shopping cart have
|
||||||
|
/// changed.
|
||||||
|
class ShoppingCartNotifier extends ChangeNotifier {
|
||||||
|
/// Notifies listeners that the products in the shopping cart have changed.
|
||||||
|
void productsChanged() {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,35 +1,31 @@
|
||||||
import "package:cached_network_image/cached_network_image.dart";
|
import "package:cached_network_image/cached_network_image.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_product_page/flutter_product_page.dart";
|
import "package:flutter_product_page/flutter_product_page.dart";
|
||||||
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
|
|
||||||
import "package:skeletonizer/skeletonizer.dart";
|
import "package:skeletonizer/skeletonizer.dart";
|
||||||
|
|
||||||
/// Product item widget.
|
/// Product item widget.
|
||||||
class DefaultProductItem extends StatelessWidget {
|
class ProductItem extends StatelessWidget {
|
||||||
/// Constructor for the product item widget.
|
/// Constructor for the product item widget.
|
||||||
const DefaultProductItem({
|
const ProductItem({
|
||||||
required this.product,
|
required this.product,
|
||||||
required this.onProductDetail,
|
required this.onProductDetail,
|
||||||
required this.onAddToCart,
|
required this.onAddToCart,
|
||||||
required this.translations,
|
required this.localizations,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Product to display.
|
/// Product to display.
|
||||||
final Product product;
|
final ProductPageProduct product;
|
||||||
|
|
||||||
/// Function to call when the product detail is requested.
|
/// Function to call when the product detail is requested.
|
||||||
final Function(
|
final Function(BuildContext context, ProductPageProduct selectedProduct)
|
||||||
BuildContext context,
|
onProductDetail;
|
||||||
Product selectedProduct,
|
|
||||||
String closeText,
|
|
||||||
) onProductDetail;
|
|
||||||
|
|
||||||
/// Function to call when the product is added to the cart.
|
/// Function to call when the product is added to the cart.
|
||||||
final Function(Product selectedProduct) onAddToCart;
|
final Function(ProductPageProduct selectedProduct) onAddToCart;
|
||||||
|
|
||||||
/// Localizations for the product page.
|
/// Localizations for the product page.
|
||||||
final ProductPageTranslations translations;
|
final ProductPageLocalization localizations;
|
||||||
|
|
||||||
/// Size of the product image.
|
/// Size of the product image.
|
||||||
static const double imageSize = 44;
|
static const double imageSize = 44;
|
||||||
|
@ -51,7 +47,7 @@ class DefaultProductItem extends StatelessWidget {
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
placeholder: (context, url) => loadingImageSkeleton,
|
placeholder: (context, url) => loadingImageSkeleton,
|
||||||
errorWidget: (context, url, error) => Tooltip(
|
errorWidget: (context, url, error) => Tooltip(
|
||||||
message: translations.failedToLoadImageExplenation,
|
message: localizations.failedToLoadImageExplenation,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 48,
|
width: 48,
|
||||||
height: 48,
|
height: 48,
|
||||||
|
@ -79,15 +75,8 @@ class DefaultProductItem extends StatelessWidget {
|
||||||
var productInformationIcon = Padding(
|
var productInformationIcon = Padding(
|
||||||
padding: const EdgeInsets.only(left: 4),
|
padding: const EdgeInsets.only(left: 4),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: () => onProductDetail(
|
onPressed: () => onProductDetail(context, product),
|
||||||
context,
|
icon: const Icon(Icons.info_outline),
|
||||||
product,
|
|
||||||
translations.close,
|
|
||||||
),
|
|
||||||
icon: Icon(
|
|
||||||
Icons.info_outline,
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -95,7 +84,10 @@ class DefaultProductItem extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
_PriceLabel(
|
_PriceLabel(
|
||||||
product: product,
|
price: product.price,
|
||||||
|
discountPrice: (product.hasDiscount && product.discountPrice != null)
|
||||||
|
? product.discountPrice
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
_AddToCardButton(
|
_AddToCardButton(
|
||||||
product: product,
|
product: product,
|
||||||
|
@ -121,33 +113,39 @@ class DefaultProductItem extends StatelessWidget {
|
||||||
|
|
||||||
class _PriceLabel extends StatelessWidget {
|
class _PriceLabel extends StatelessWidget {
|
||||||
const _PriceLabel({
|
const _PriceLabel({
|
||||||
required this.product,
|
required this.price,
|
||||||
|
required this.discountPrice,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Product product;
|
final double price;
|
||||||
|
final double? discountPrice;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var theme = Theme.of(context);
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
if (discountPrice == null)
|
||||||
|
return Text(
|
||||||
|
price.toStringAsFixed(2),
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
);
|
||||||
|
else
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
if (product.hasDiscount) ...[
|
|
||||||
Text(
|
Text(
|
||||||
product.price.toStringAsFixed(2),
|
price.toStringAsFixed(2),
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
fontSize: 10,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
decoration: TextDecoration.lineThrough,
|
decoration: TextDecoration.lineThrough,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
Padding(
|
||||||
],
|
padding: const EdgeInsets.only(left: 4.0),
|
||||||
Text(
|
child: Text(
|
||||||
product.hasDiscount
|
discountPrice!.toStringAsFixed(2),
|
||||||
? product.discountPrice!.toStringAsFixed(2)
|
style: theme.textTheme.bodyMedium,
|
||||||
: product.price.toStringAsFixed(2),
|
),
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -160,30 +158,36 @@ class _AddToCardButton extends StatelessWidget {
|
||||||
required this.onAddToCart,
|
required this.onAddToCart,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Product product;
|
final ProductPageProduct product;
|
||||||
final Function(Product product) onAddToCart;
|
final Function(ProductPageProduct product) onAddToCart;
|
||||||
|
|
||||||
static const double boxSize = 29;
|
static const double boxSize = 29;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var theme = Theme.of(context);
|
var theme = Theme.of(context);
|
||||||
return Container(
|
return SizedBox(
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
width: boxSize,
|
width: boxSize,
|
||||||
height: boxSize,
|
height: boxSize,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
icon: const Icon(
|
icon: Icon(
|
||||||
Icons.add,
|
Icons.add,
|
||||||
color: Colors.white,
|
color: theme.primaryColor,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
onPressed: () => onAddToCart(product),
|
onPressed: () => onAddToCart(product),
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: WidgetStateProperty.all(
|
||||||
|
theme.colorScheme.secondary,
|
||||||
|
),
|
||||||
|
shape: WidgetStateProperty.all(
|
||||||
|
RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
|
@ -0,0 +1,63 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_product_page/flutter_product_page.dart";
|
||||||
|
import "package:flutter_product_page/src/services/selected_shop_service.dart";
|
||||||
|
import "package:flutter_product_page/src/ui/widgets/horizontal_list_items.dart";
|
||||||
|
import "package:flutter_product_page/src/ui/widgets/spaced_wrap.dart";
|
||||||
|
|
||||||
|
/// Shop selector widget that displays a list to navigate between shops.
|
||||||
|
class ShopSelector extends StatelessWidget {
|
||||||
|
/// Constructor for the shop selector.
|
||||||
|
const ShopSelector({
|
||||||
|
required this.configuration,
|
||||||
|
required this.selectedShopService,
|
||||||
|
required this.shops,
|
||||||
|
required this.onTap,
|
||||||
|
this.paddingBetweenButtons = 4,
|
||||||
|
this.paddingOnButtons = 8,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Configuration for the product page.
|
||||||
|
final ProductPageConfiguration configuration;
|
||||||
|
|
||||||
|
/// Service for the selected shop.
|
||||||
|
final SelectedShopService selectedShopService;
|
||||||
|
|
||||||
|
/// List of shops.
|
||||||
|
final List<ProductPageShop> shops;
|
||||||
|
|
||||||
|
/// Callback when a shop is tapped.
|
||||||
|
final Function(ProductPageShop shop) onTap;
|
||||||
|
|
||||||
|
/// Padding between the buttons.
|
||||||
|
final double paddingBetweenButtons;
|
||||||
|
|
||||||
|
/// Padding on the buttons.
|
||||||
|
final double paddingOnButtons;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (shops.length == 1) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.shopSelectorStyle == ShopSelectorStyle.spacedWrap) {
|
||||||
|
return SpacedWrap(
|
||||||
|
shops: shops,
|
||||||
|
selectedItem: selectedShopService.selectedShop!.id,
|
||||||
|
onTap: onTap,
|
||||||
|
width: MediaQuery.of(context).size.width - (16 * 2),
|
||||||
|
paddingBetweenButtons: paddingBetweenButtons,
|
||||||
|
paddingOnButtons: paddingOnButtons,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return HorizontalListItems(
|
||||||
|
shops: shops,
|
||||||
|
selectedItem: selectedShopService.selectedShop!.id,
|
||||||
|
onTap: onTap,
|
||||||
|
paddingBetweenButtons: paddingBetweenButtons,
|
||||||
|
paddingOnButtons: paddingOnButtons,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
import "package:cached_network_image/cached_network_image.dart";
|
import "package:cached_network_image/cached_network_image.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_product_page/flutter_product_page.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.
|
/// A widget that displays a weekly discount.
|
||||||
class WeeklyDiscount extends StatelessWidget {
|
class WeeklyDiscount extends StatelessWidget {
|
||||||
|
@ -16,10 +15,10 @@ class WeeklyDiscount extends StatelessWidget {
|
||||||
final ProductPageConfiguration configuration;
|
final ProductPageConfiguration configuration;
|
||||||
|
|
||||||
/// The product for which the discount is displayed.
|
/// The product for which the discount is displayed.
|
||||||
final Product product;
|
final ProductPageProduct product;
|
||||||
|
|
||||||
/// The top padding of the widget.
|
/// The top padding of the widget.
|
||||||
static const double topPadding = 20;
|
static const double topPadding = 32.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -28,8 +27,10 @@ class WeeklyDiscount extends StatelessWidget {
|
||||||
var bottomText = Padding(
|
var bottomText = Padding(
|
||||||
padding: const EdgeInsets.all(20.0),
|
padding: const EdgeInsets.all(20.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
configuration.discountDescription!(product),
|
configuration.getDiscountDescription!(product),
|
||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -51,7 +52,7 @@ class WeeklyDiscount extends StatelessWidget {
|
||||||
Icons.error_outline_rounded,
|
Icons.error_outline_rounded,
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
),
|
),
|
||||||
Text(configuration.translations.failedToLoadImageExplenation),
|
Text(configuration.localizations.failedToLoadImageExplenation),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -72,9 +73,9 @@ class WeeklyDiscount extends StatelessWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
var topText = DecoratedBox(
|
var topText = DecoratedBox(
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black,
|
color: theme.primaryColor,
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
topLeft: Radius.circular(4),
|
topLeft: Radius.circular(4),
|
||||||
topRight: Radius.circular(4),
|
topRight: Radius.circular(4),
|
||||||
),
|
),
|
||||||
|
@ -87,8 +88,10 @@ class WeeklyDiscount extends StatelessWidget {
|
||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
configuration.translations.discountTitle,
|
configuration.localizations.discountTitle.toUpperCase(),
|
||||||
style: theme.textTheme.headlineSmall,
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
),
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -97,6 +100,7 @@ class WeeklyDiscount extends StatelessWidget {
|
||||||
|
|
||||||
var boxDecoration = BoxDecoration(
|
var boxDecoration = BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
|
color: theme.primaryColor,
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(4.0),
|
borderRadius: BorderRadius.circular(4.0),
|
288
packages/flutter_product_page/lib/src/ui/product_page.dart
Normal file
288
packages/flutter_product_page/lib/src/ui/product_page.dart
Normal file
|
@ -0,0 +1,288 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_product_page/flutter_product_page.dart";
|
||||||
|
import "package:flutter_product_page/src/services/category_service.dart";
|
||||||
|
import "package:flutter_product_page/src/services/selected_shop_service.dart";
|
||||||
|
import "package:flutter_product_page/src/services/shopping_cart_notifier.dart";
|
||||||
|
import "package:flutter_product_page/src/ui/components/shop_selector.dart";
|
||||||
|
import "package:flutter_product_page/src/ui/components/weekly_discount.dart";
|
||||||
|
|
||||||
|
/// A page that displays products.
|
||||||
|
class ProductPage extends StatelessWidget {
|
||||||
|
/// Constructor for the product page.
|
||||||
|
ProductPage({
|
||||||
|
required this.configuration,
|
||||||
|
this.initialBuildShopId,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Configuration for the product page.
|
||||||
|
final ProductPageConfiguration configuration;
|
||||||
|
|
||||||
|
/// An optional initial shop ID to select. This overrides the initialShopId
|
||||||
|
/// from the configuration.
|
||||||
|
final String? initialBuildShopId;
|
||||||
|
|
||||||
|
late final SelectedShopService _selectedShopService = SelectedShopService();
|
||||||
|
|
||||||
|
late final ShoppingCartNotifier _shoppingCartNotifier =
|
||||||
|
ShoppingCartNotifier();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Padding(
|
||||||
|
padding: configuration.pagePadding,
|
||||||
|
child: FutureBuilder(
|
||||||
|
future: configuration.shops,
|
||||||
|
builder: (BuildContext context, AsyncSnapshot data) {
|
||||||
|
if (data.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.hasError) {
|
||||||
|
return configuration.errorBuilder!(
|
||||||
|
context,
|
||||||
|
data.error,
|
||||||
|
data.stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ProductPageShop>? shops = data.data;
|
||||||
|
|
||||||
|
if (shops == null || shops.isEmpty) {
|
||||||
|
return configuration.errorBuilder!(context, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initialBuildShopId != null) {
|
||||||
|
ProductPageShop? initialShop;
|
||||||
|
|
||||||
|
for (var shop in shops) {
|
||||||
|
if (shop.id == initialBuildShopId) {
|
||||||
|
initialShop = shop;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectedShopService.selectShop(initialShop ?? shops.first);
|
||||||
|
} else if (configuration.initialShopId != null) {
|
||||||
|
ProductPageShop? initialShop;
|
||||||
|
|
||||||
|
for (var shop in shops) {
|
||||||
|
if (shop.id == configuration.initialShopId) {
|
||||||
|
initialShop = shop;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectedShopService.selectShop(initialShop ?? shops.first);
|
||||||
|
} else {
|
||||||
|
_selectedShopService.selectShop(shops.first);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListenableBuilder(
|
||||||
|
listenable: _selectedShopService,
|
||||||
|
builder: (BuildContext context, Widget? _) {
|
||||||
|
configuration.onShopSelectionChange?.call(
|
||||||
|
_selectedShopService.selectedShop!,
|
||||||
|
);
|
||||||
|
return _ProductPage(
|
||||||
|
configuration: configuration,
|
||||||
|
selectedShopService: _selectedShopService,
|
||||||
|
shoppingCartNotifier: _shoppingCartNotifier,
|
||||||
|
shops: shops,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProductPage extends StatelessWidget {
|
||||||
|
const _ProductPage({
|
||||||
|
required this.configuration,
|
||||||
|
required this.selectedShopService,
|
||||||
|
required this.shoppingCartNotifier,
|
||||||
|
required this.shops,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ProductPageConfiguration configuration;
|
||||||
|
final SelectedShopService selectedShopService;
|
||||||
|
final ShoppingCartNotifier shoppingCartNotifier;
|
||||||
|
|
||||||
|
final List<ProductPageShop> shops;
|
||||||
|
|
||||||
|
void _onTapChangeShop(ProductPageShop shop) {
|
||||||
|
selectedShopService.selectShop(shop);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var pageContent = SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ShopSelector(
|
||||||
|
configuration: configuration,
|
||||||
|
selectedShopService: selectedShopService,
|
||||||
|
shops: shops,
|
||||||
|
onTap: _onTapChangeShop,
|
||||||
|
),
|
||||||
|
_ShopContents(
|
||||||
|
configuration: configuration,
|
||||||
|
selectedShopService: selectedShopService,
|
||||||
|
shoppingCartNotifier: shoppingCartNotifier,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
pageContent,
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: configuration.navigateToShoppingCartBuilder != null
|
||||||
|
? configuration.navigateToShoppingCartBuilder!(context)
|
||||||
|
: _NavigateToShoppingCartButton(
|
||||||
|
configuration: configuration,
|
||||||
|
shoppingCartNotifier: shoppingCartNotifier,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NavigateToShoppingCartButton extends StatelessWidget {
|
||||||
|
const _NavigateToShoppingCartButton({
|
||||||
|
required this.configuration,
|
||||||
|
required this.shoppingCartNotifier,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ProductPageConfiguration configuration;
|
||||||
|
final ShoppingCartNotifier shoppingCartNotifier;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
String getProductsInShoppingCartLabel() {
|
||||||
|
var fun = configuration.getProductsInShoppingCart;
|
||||||
|
|
||||||
|
if (fun == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "(${fun()})";
|
||||||
|
}
|
||||||
|
|
||||||
|
return FilledButton(
|
||||||
|
onPressed: configuration.onNavigateToShoppingCart,
|
||||||
|
style: theme.filledButtonTheme.style?.copyWith(
|
||||||
|
backgroundColor: WidgetStateProperty.all(
|
||||||
|
theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16.0,
|
||||||
|
vertical: 8.0,
|
||||||
|
),
|
||||||
|
child: ListenableBuilder(
|
||||||
|
listenable: shoppingCartNotifier,
|
||||||
|
builder: (BuildContext context, Widget? _) => Text(
|
||||||
|
"""${configuration.localizations.navigateToShoppingCart.toUpperCase()} ${getProductsInShoppingCartLabel()}""",
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShopContents extends StatelessWidget {
|
||||||
|
const _ShopContents({
|
||||||
|
required this.configuration,
|
||||||
|
required this.selectedShopService,
|
||||||
|
required this.shoppingCartNotifier,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ProductPageConfiguration configuration;
|
||||||
|
final SelectedShopService selectedShopService;
|
||||||
|
final ShoppingCartNotifier shoppingCartNotifier;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Padding(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: configuration.pagePadding.horizontal,
|
||||||
|
),
|
||||||
|
child: FutureBuilder(
|
||||||
|
// ignore: discarded_futures
|
||||||
|
future: configuration.getProducts(
|
||||||
|
selectedShopService.selectedShop!,
|
||||||
|
),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return configuration.errorBuilder!(
|
||||||
|
context,
|
||||||
|
snapshot.error,
|
||||||
|
snapshot.stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var productPageContent = snapshot.data;
|
||||||
|
|
||||||
|
if (productPageContent == null ||
|
||||||
|
productPageContent.products.isEmpty) {
|
||||||
|
return configuration.noContentBuilder!(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
var productList = Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Products
|
||||||
|
getCategoryList(
|
||||||
|
context,
|
||||||
|
configuration,
|
||||||
|
shoppingCartNotifier,
|
||||||
|
productPageContent.products,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Bottom padding so the last product is not cut off
|
||||||
|
// by the to shopping cart button.
|
||||||
|
const SizedBox(height: 48),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
// Discounted product
|
||||||
|
if (productPageContent.discountedProduct != null) ...[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: WeeklyDiscount(
|
||||||
|
configuration: configuration,
|
||||||
|
product: productPageContent.discountedProduct!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
productList,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_product_page/src/configuration/product_page_configuration.dart";
|
||||||
|
import "package:flutter_product_page/src/ui/product_page.dart";
|
||||||
|
|
||||||
|
/// A screen that displays a product page. This screen contains a Scaffold,
|
||||||
|
/// in which the body is a SafeArea that contains a ProductPage widget.
|
||||||
|
///
|
||||||
|
/// If you do not wish to create a Scaffold you can use the
|
||||||
|
/// [ProductPage] widget directly.
|
||||||
|
class ProductPageScreen extends StatelessWidget {
|
||||||
|
/// Constructor for the product page screen.
|
||||||
|
const ProductPageScreen({
|
||||||
|
required this.configuration,
|
||||||
|
this.initialBuildShopId,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Configuration for the product page.
|
||||||
|
final ProductPageConfiguration configuration;
|
||||||
|
|
||||||
|
/// An optional initial shop ID to select. This overrides the initialShopId
|
||||||
|
/// from the configuration.
|
||||||
|
final String? initialBuildShopId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
body: SafeArea(
|
||||||
|
child: ProductPage(
|
||||||
|
configuration: configuration,
|
||||||
|
initialBuildShopId: initialBuildShopId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
appBar: configuration.appBar,
|
||||||
|
bottomNavigationBar: configuration.bottomNavigationBar,
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_product_page/flutter_product_page.dart";
|
||||||
|
|
||||||
|
/// Horizontal list of items.
|
||||||
|
class HorizontalListItems extends StatelessWidget {
|
||||||
|
/// Constructor for the horizontal list of items.
|
||||||
|
const HorizontalListItems({
|
||||||
|
required this.shops,
|
||||||
|
required this.selectedItem,
|
||||||
|
required this.onTap,
|
||||||
|
this.paddingBetweenButtons = 2.0,
|
||||||
|
this.paddingOnButtons = 4,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// List of items.
|
||||||
|
final List<ProductPageShop> shops;
|
||||||
|
|
||||||
|
/// Selected item.
|
||||||
|
final String selectedItem;
|
||||||
|
|
||||||
|
/// Padding between the buttons.
|
||||||
|
final double paddingBetweenButtons;
|
||||||
|
|
||||||
|
/// Padding on the buttons.
|
||||||
|
final double paddingOnButtons;
|
||||||
|
|
||||||
|
/// Callback when an item is tapped.
|
||||||
|
final Function(ProductPageShop shop) onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: shops
|
||||||
|
.map(
|
||||||
|
(shop) => Padding(
|
||||||
|
padding: EdgeInsets.only(right: paddingBetweenButtons),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => onTap(shop),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: shop.id == selectedItem
|
||||||
|
? theme.colorScheme.primary
|
||||||
|
: theme.colorScheme.secondary,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.all(paddingOnButtons),
|
||||||
|
child: Text(
|
||||||
|
shop.name,
|
||||||
|
style: shop.id == selectedItem
|
||||||
|
? theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
)
|
||||||
|
: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_product_page/flutter_product_page.dart";
|
||||||
|
|
||||||
|
/// A popup that displays the product item.
|
||||||
|
class ProductItemPopup extends StatelessWidget {
|
||||||
|
/// Constructor for the product item popup.
|
||||||
|
const ProductItemPopup({
|
||||||
|
required this.product,
|
||||||
|
required this.configuration,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The product to display.
|
||||||
|
final ProductPageProduct product;
|
||||||
|
|
||||||
|
/// Configuration for the product page.
|
||||||
|
final ProductPageConfiguration configuration;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
var productDescription = Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(44, 32, 44, 20),
|
||||||
|
child: Text(
|
||||||
|
product.name,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
var closeButton = Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(80, 0, 80, 32),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 254,
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: theme.elevatedButtonTheme.style?.copyWith(
|
||||||
|
shape: WidgetStateProperty.all(
|
||||||
|
RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
child: Text(
|
||||||
|
configuration.localizations.close,
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
productDescription,
|
||||||
|
closeButton,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,150 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_product_page/flutter_product_page.dart";
|
||||||
|
|
||||||
|
/// SpacedWrap is a widget that wraps a list of items that are spaced out and
|
||||||
|
/// fill the available width.
|
||||||
|
class SpacedWrap extends StatelessWidget {
|
||||||
|
/// Creates a [SpacedWrap].
|
||||||
|
const SpacedWrap({
|
||||||
|
required this.shops,
|
||||||
|
required this.onTap,
|
||||||
|
required this.width,
|
||||||
|
this.paddingBetweenButtons = 2.0,
|
||||||
|
this.paddingOnButtons = 4.0,
|
||||||
|
this.selectedItem = "",
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// List of items.
|
||||||
|
final List<ProductPageShop> shops;
|
||||||
|
|
||||||
|
/// Selected item.
|
||||||
|
final String selectedItem;
|
||||||
|
|
||||||
|
/// Width of the widget.
|
||||||
|
final double width;
|
||||||
|
|
||||||
|
/// Padding between the buttons.
|
||||||
|
final double paddingBetweenButtons;
|
||||||
|
|
||||||
|
/// Padding on the buttons.
|
||||||
|
final double paddingOnButtons;
|
||||||
|
|
||||||
|
/// Callback when an item is tapped.
|
||||||
|
final Function(ProductPageShop shop) onTap;
|
||||||
|
|
||||||
|
Row _buildRow(
|
||||||
|
BuildContext context,
|
||||||
|
List<int> currentRow,
|
||||||
|
double availableRowLength,
|
||||||
|
) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
var row = <Widget>[];
|
||||||
|
var extraButtonPadding = availableRowLength / currentRow.length / 2;
|
||||||
|
|
||||||
|
for (var i = 0, len = currentRow.length; i < len; i++) {
|
||||||
|
var shop = shops[currentRow[i]];
|
||||||
|
row.add(
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(top: paddingBetweenButtons),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => onTap(shop),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: shop.id == selectedItem
|
||||||
|
? theme.colorScheme.primary
|
||||||
|
: theme.colorScheme.secondary,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: paddingOnButtons + extraButtonPadding,
|
||||||
|
vertical: paddingOnButtons,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
shop.name,
|
||||||
|
style: shop.id == selectedItem
|
||||||
|
? theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
)
|
||||||
|
: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (shops.last != shop) {
|
||||||
|
row.add(const Spacer());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: row,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Row> _buildButtonRows(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
var rows = <Row>[];
|
||||||
|
var currentRow = <int>[];
|
||||||
|
var availableRowLength = width;
|
||||||
|
|
||||||
|
for (var i = 0; i < shops.length; i++) {
|
||||||
|
var shop = shops[i];
|
||||||
|
|
||||||
|
var textPainter = TextPainter(
|
||||||
|
text: TextSpan(
|
||||||
|
text: shop.name,
|
||||||
|
style: shop.id == selectedItem
|
||||||
|
? theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
)
|
||||||
|
: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
)..layout(minWidth: 0, maxWidth: double.infinity);
|
||||||
|
|
||||||
|
var buttonWidth = textPainter.width + paddingOnButtons * 2;
|
||||||
|
|
||||||
|
if (availableRowLength - buttonWidth < 0) {
|
||||||
|
rows.add(
|
||||||
|
_buildRow(
|
||||||
|
context,
|
||||||
|
currentRow,
|
||||||
|
availableRowLength,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
currentRow = <int>[];
|
||||||
|
availableRowLength = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentRow.add(i);
|
||||||
|
|
||||||
|
availableRowLength -= buttonWidth + paddingBetweenButtons;
|
||||||
|
}
|
||||||
|
if (currentRow.isNotEmpty) {
|
||||||
|
rows.add(
|
||||||
|
_buildRow(
|
||||||
|
context,
|
||||||
|
currentRow,
|
||||||
|
availableRowLength,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Column(
|
||||||
|
children: _buildButtonRows(
|
||||||
|
context,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -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,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,10 +1,10 @@
|
||||||
name: flutter_product_page
|
name: flutter_product_page
|
||||||
description: "A Flutter module for the product page"
|
description: "A Flutter module for the product page"
|
||||||
publish_to: "none"
|
publish_to: 'none'
|
||||||
version: 2.0.0
|
version: 1.0.0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.3.4 <4.0.0"
|
sdk: '>=3.3.4 <4.0.0'
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
|
@ -15,13 +15,6 @@ dependencies:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_nested_categories
|
url: https://github.com/Iconica-Development/flutter_nested_categories
|
||||||
ref: 0.0.1
|
ref: 0.0.1
|
||||||
flutter_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:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@ -33,3 +26,13 @@ dev_dependencies:
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
|
# assets:
|
||||||
|
# - images/a_dot_burr.jpeg
|
||||||
|
# - images/a_dot_ham.jpeg
|
||||||
|
# fonts:
|
||||||
|
# - family: Schyler
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/Schyler-Regular.ttf
|
||||||
|
# - asset: fonts/Schyler-Italic.ttf
|
||||||
|
# style: italic
|
||||||
|
|
53
packages/flutter_shopping/example/.gitignore
vendored
Normal file
53
packages/flutter_shopping/example/.gitignore
vendored
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
.metadata
|
||||||
|
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
|
||||||
|
|
||||||
|
# Platforms
|
||||||
|
android/
|
||||||
|
ios/
|
||||||
|
linux/
|
||||||
|
macos/
|
||||||
|
web/
|
||||||
|
windows/
|
16
packages/flutter_shopping/example/README.md
Normal file
16
packages/flutter_shopping/example/README.md
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# example
|
||||||
|
|
||||||
|
A new Flutter project.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This project is a starting point for a Flutter application.
|
||||||
|
|
||||||
|
A few resources to get you started if this is your first Flutter project:
|
||||||
|
|
||||||
|
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||||
|
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||||
|
|
||||||
|
For help getting started with Flutter development, view the
|
||||||
|
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||||
|
samples, guidance on mobile development, and a full API reference.
|
7
packages/flutter_shopping/example/analysis_options.yaml
Normal file
7
packages/flutter_shopping/example/analysis_options.yaml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
include: package:flutter_iconica_analysis/analysis_options.yaml
|
||||||
|
|
||||||
|
analyzer:
|
||||||
|
exclude:
|
||||||
|
|
||||||
|
linter:
|
||||||
|
rules:
|
22
packages/flutter_shopping/example/lib/main.dart
Normal file
22
packages/flutter_shopping/example/lib/main.dart
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import "package:example/src/routes.dart";
|
||||||
|
import "package:example/src/utils/theme.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
runApp(const ProviderScope(child: MyApp()));
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyApp extends HookConsumerWidget {
|
||||||
|
const MyApp({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) => MaterialApp.router(
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
restorationScopeId: "app",
|
||||||
|
theme: getTheme(),
|
||||||
|
routerConfig: ref.read(routerProvider),
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,193 @@
|
||||||
|
import "package:example/src/models/my_product.dart";
|
||||||
|
import "package:example/src/routes.dart";
|
||||||
|
import "package:example/src/services/order_service.dart";
|
||||||
|
import "package:example/src/services/shop_service.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
import "package:go_router/go_router.dart";
|
||||||
|
|
||||||
|
// (REQUIRED): Create your own instance of the ProductService.
|
||||||
|
final ProductService<MyProduct> productService = ProductService([]);
|
||||||
|
|
||||||
|
FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
|
||||||
|
FlutterShoppingConfiguration(
|
||||||
|
// (REQUIRED): Shop builder configuration
|
||||||
|
shopBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
String? initialBuildShopId,
|
||||||
|
String? streetName,
|
||||||
|
) =>
|
||||||
|
ProductPageScreen(
|
||||||
|
configuration: ProductPageConfiguration(
|
||||||
|
// (REQUIRED): List of shops that should be displayed
|
||||||
|
// If there is only one, make a list with just one shop.
|
||||||
|
shops: Future.value(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: (ProductPageShop shop) =>
|
||||||
|
Future<ProductPageContent>.value(
|
||||||
|
getShopContent(shop.id),
|
||||||
|
),
|
||||||
|
|
||||||
|
// (REQUIRED): Function to navigate to the shopping cart
|
||||||
|
onNavigateToShoppingCart: () => onCompleteProductPage(context),
|
||||||
|
|
||||||
|
// (RECOMMENDED): Function to get the number of products in the
|
||||||
|
// shopping cart. This is used to display the number of products
|
||||||
|
// in the shopping cart on the product page.
|
||||||
|
getProductsInShoppingCart: productService.countProducts,
|
||||||
|
|
||||||
|
// (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.
|
||||||
|
initialShopId: getShops().first.id,
|
||||||
|
|
||||||
|
// (RECOMMENDED) Localizations for the product page.
|
||||||
|
localizations: const ProductPageLocalization(),
|
||||||
|
|
||||||
|
// (OPTIONAL) Appbar
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Shop"),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.arrow_back,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
context.go(homePage);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// (OPTIONAL): Initial build shop id that overrides the initialShop
|
||||||
|
initialBuildShopId: initialBuildShopId,
|
||||||
|
),
|
||||||
|
|
||||||
|
// (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) => ListTile(
|
||||||
|
title: Text(product.name),
|
||||||
|
subtitle: Text(product.price.toStringAsFixed(2)),
|
||||||
|
leading: Image.network(
|
||||||
|
product.imageUrl,
|
||||||
|
errorBuilder: (context, error, stackTrace) => const Tooltip(
|
||||||
|
message: "Error loading image",
|
||||||
|
child: Icon(
|
||||||
|
Icons.error,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.remove),
|
||||||
|
onPressed: () => productService.removeOneProduct(product),
|
||||||
|
),
|
||||||
|
Text("${product.quantity}"),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
onPressed: () => productService.addProduct(product),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// (OPTIONAL/REQUIRED) on confirm order callback:
|
||||||
|
// Either use this callback or the placeOrderButtonBuilder.
|
||||||
|
onConfirmOrder: (products) => onCompleteShoppingCart(context),
|
||||||
|
|
||||||
|
// (RECOMMENDED) localizations:
|
||||||
|
localizations: const ShoppingCartLocalizations(),
|
||||||
|
|
||||||
|
// (OPTIONAL) title above product list:
|
||||||
|
title: "Products",
|
||||||
|
|
||||||
|
/// (OPTIONAL) no content builder for when there are no products
|
||||||
|
/// in the shopping cart.
|
||||||
|
noContentBuilder: (context) => const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 128),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.warning,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Geen producten in winkelmandje",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// (OPTIONAL) custom appbar:
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Shopping Cart"),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.arrow_back,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
context.go(FlutterShoppingPathRoutes.shop);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// (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 {
|
||||||
|
if (orderDetails.order["payment_option"] == "Pay now") {
|
||||||
|
// Make the user pay upfront.
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all went well, we can store the order in the database.
|
||||||
|
// Make sure to register whether or not the order was paid.
|
||||||
|
storeOrderInDatabase(productService.products, orderDetails);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
|
@ -0,0 +1,25 @@
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
|
||||||
|
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
|
||||||
|
final String category;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String imageUrl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final double? discountPrice;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final bool hasDiscount;
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
|
||||||
|
class MyShop extends ProductPageShop {
|
||||||
|
const MyShop({
|
||||||
|
required super.id,
|
||||||
|
required super.name,
|
||||||
|
});
|
||||||
|
}
|
31
packages/flutter_shopping/example/lib/src/routes.dart
Normal file
31
packages/flutter_shopping/example/lib/src/routes.dart
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import "package:example/src/configuration/configuration.dart";
|
||||||
|
import "package:example/src/ui/homepage.dart";
|
||||||
|
import "package:example/src/utils/go_router.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
import "package:go_router/go_router.dart";
|
||||||
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
|
||||||
|
const String homePage = "/";
|
||||||
|
|
||||||
|
final routerProvider = Provider<GoRouter>(
|
||||||
|
(ref) => GoRouter(
|
||||||
|
initialLocation: homePage,
|
||||||
|
routes: [
|
||||||
|
// Flutter Shopping Story Routes
|
||||||
|
...getShoppingStoryRoutes(
|
||||||
|
configuration: getFlutterShoppingConfiguration(),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Home Route
|
||||||
|
GoRoute(
|
||||||
|
name: "home",
|
||||||
|
path: homePage,
|
||||||
|
pageBuilder: (context, state) => buildScreenWithFadeTransition(
|
||||||
|
context: context,
|
||||||
|
state: state,
|
||||||
|
child: const Homepage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
|
@ -0,0 +1,7 @@
|
||||||
|
import "package:example/src/models/my_product.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
|
||||||
|
/// Example implementation of storing an order in a database.
|
||||||
|
void storeOrderInDatabase(List<MyProduct> products, OrderResult result) {
|
||||||
|
return;
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
import "package:example/src/models/my_product.dart";
|
||||||
|
import "package:example/src/models/my_shop.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
|
||||||
|
/// This function should have your own implementation. Generally this would
|
||||||
|
/// contain some API call to fetch the list of shops.
|
||||||
|
List<MyShop> getShops() => <MyShop>[
|
||||||
|
const MyShop(id: "1", name: "Shop 1"),
|
||||||
|
const MyShop(id: "2", name: "Shop 2"),
|
||||||
|
const MyShop(id: "3", name: "Shop 3"),
|
||||||
|
];
|
||||||
|
|
||||||
|
ProductPageContent getShopContent(String shopId) {
|
||||||
|
var products = getProducts(shopId);
|
||||||
|
return ProductPageContent(
|
||||||
|
discountedProduct: products.first,
|
||||||
|
products: products,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function should have your own implementation. Generally this would
|
||||||
|
/// contain some API call to fetch the list of products for a shop.
|
||||||
|
List<MyProduct> getProducts(String shopId) => <MyProduct>[
|
||||||
|
MyProduct(
|
||||||
|
id: "1",
|
||||||
|
name: "White bread",
|
||||||
|
price: 2.99,
|
||||||
|
category: "Loaves",
|
||||||
|
imageUrl: "https://via.placeholder.com/150",
|
||||||
|
hasDiscount: true,
|
||||||
|
discountPrice: 1.99,
|
||||||
|
),
|
||||||
|
MyProduct(
|
||||||
|
id: "2",
|
||||||
|
name: "Brown bread",
|
||||||
|
price: 2.99,
|
||||||
|
category: "Loaves",
|
||||||
|
imageUrl: "https://via.placeholder.com/150",
|
||||||
|
),
|
||||||
|
MyProduct(
|
||||||
|
id: "3",
|
||||||
|
name: "Cheese sandwich",
|
||||||
|
price: 1.99,
|
||||||
|
category: "Sandwiches",
|
||||||
|
imageUrl: "https://via.placeholder.com/150",
|
||||||
|
),
|
||||||
|
];
|
20
packages/flutter_shopping/example/lib/src/ui/homepage.dart
Normal file
20
packages/flutter_shopping/example/lib/src/ui/homepage.dart
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
import "package:go_router/go_router.dart";
|
||||||
|
|
||||||
|
class Homepage extends StatelessWidget {
|
||||||
|
const Homepage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Badge(
|
||||||
|
label: const Text("1"),
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.shopping_cart_outlined, size: 50),
|
||||||
|
onPressed: () => context.go(FlutterShoppingPathRoutes.shop),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:go_router/go_router.dart";
|
||||||
|
|
||||||
|
CustomTransitionPage buildScreenWithFadeTransition<T>({
|
||||||
|
required BuildContext context,
|
||||||
|
required GoRouterState state,
|
||||||
|
required Widget child,
|
||||||
|
}) =>
|
||||||
|
CustomTransitionPage<T>(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: child,
|
||||||
|
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
|
||||||
|
FadeTransition(opacity: animation, child: child),
|
||||||
|
);
|
||||||
|
|
||||||
|
CustomTransitionPage buildScreenWithoutTransition<T>({
|
||||||
|
required BuildContext context,
|
||||||
|
required GoRouterState state,
|
||||||
|
required Widget child,
|
||||||
|
}) =>
|
||||||
|
CustomTransitionPage<T>(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: child,
|
||||||
|
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
|
||||||
|
child,
|
||||||
|
);
|
32
packages/flutter_shopping/example/lib/src/utils/theme.dart
Normal file
32
packages/flutter_shopping/example/lib/src/utils/theme.dart
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
|
ThemeData getTheme() => ThemeData(
|
||||||
|
scaffoldBackgroundColor: const Color.fromRGBO(250, 249, 246, 1),
|
||||||
|
textTheme: const TextTheme(
|
||||||
|
labelMedium: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
titleMedium: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Color.fromRGBO(60, 60, 59, 1),
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
inputDecorationTheme: const InputDecorationTheme(
|
||||||
|
fillColor: Colors.white,
|
||||||
|
),
|
||||||
|
colorScheme: const ColorScheme.light(
|
||||||
|
primary: Color.fromRGBO(64, 87, 122, 1),
|
||||||
|
secondary: Colors.white,
|
||||||
|
surface: Color.fromRGBO(250, 249, 246, 1),
|
||||||
|
),
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
backgroundColor: Color.fromRGBO(64, 87, 122, 1),
|
||||||
|
titleTextStyle: TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
41
packages/flutter_shopping/example/pubspec.yaml
Normal file
41
packages/flutter_shopping/example/pubspec.yaml
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
name: example
|
||||||
|
description: Demonstrates how to use the flutter_shopping package."
|
||||||
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
|
version: 1.0.0
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: '>=3.3.4 <4.0.0'
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_hooks: ^0.20.0
|
||||||
|
hooks_riverpod: ^2.1.1
|
||||||
|
go_router: 12.1.3
|
||||||
|
|
||||||
|
# Iconica packages
|
||||||
|
|
||||||
|
## Userstories
|
||||||
|
flutter_shopping:
|
||||||
|
path: ../
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_iconica_analysis:
|
||||||
|
git:
|
||||||
|
url: https://github.com/Iconica-Development/flutter_iconica_analysis
|
||||||
|
ref: 7.0.0
|
||||||
|
|
||||||
|
flutter:
|
||||||
|
uses-material-design: true
|
||||||
|
# assets:
|
||||||
|
# - images/a_dot_burr.jpeg
|
||||||
|
# - images/a_dot_ham.jpeg
|
||||||
|
|
||||||
|
# fonts:
|
||||||
|
# - family: Schyler
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/Schyler-Regular.ttf
|
||||||
|
# - asset: fonts/Schyler-Italic.ttf
|
||||||
|
# style: italic
|
53
packages/flutter_shopping/example_amazon/.gitignore
vendored
Normal file
53
packages/flutter_shopping/example_amazon/.gitignore
vendored
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
.metadata
|
||||||
|
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
|
||||||
|
|
||||||
|
# Platforms
|
||||||
|
/android/
|
||||||
|
/ios/
|
||||||
|
/linux/
|
||||||
|
/macos/
|
||||||
|
/web/
|
||||||
|
/windows/
|
16
packages/flutter_shopping/example_amazon/README.md
Normal file
16
packages/flutter_shopping/example_amazon/README.md
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# amazon
|
||||||
|
|
||||||
|
A new Flutter project.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This project is a starting point for a Flutter application.
|
||||||
|
|
||||||
|
A few resources to get you started if this is your first Flutter project:
|
||||||
|
|
||||||
|
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||||
|
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||||
|
|
||||||
|
For help getting started with Flutter development, view the
|
||||||
|
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||||
|
samples, guidance on mobile development, and a full API reference.
|
|
@ -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
|
22
packages/flutter_shopping/example_amazon/lib/main.dart
Normal file
22
packages/flutter_shopping/example_amazon/lib/main.dart
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import "package:amazon/src/routes.dart";
|
||||||
|
import "package:amazon/src/utils/theme.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
runApp(const ProviderScope(child: MyApp()));
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyApp extends HookConsumerWidget {
|
||||||
|
const MyApp({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) => MaterialApp.router(
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
restorationScopeId: "app",
|
||||||
|
theme: getTheme(),
|
||||||
|
routerConfig: ref.read(routerProvider),
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,359 @@
|
||||||
|
import "package:amazon/src/models/my_product.dart";
|
||||||
|
import "package:amazon/src/routes.dart";
|
||||||
|
import "package:amazon/src/services/category_service.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
import "package:go_router/go_router.dart";
|
||||||
|
|
||||||
|
// (REQUIRED): Create your own instance of the ProductService.
|
||||||
|
final ProductService<MyProduct> productService = ProductService([]);
|
||||||
|
|
||||||
|
FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
|
||||||
|
FlutterShoppingConfiguration(
|
||||||
|
// (REQUIRED): Shop builder configuration
|
||||||
|
shopBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
String? initialBuildShopId,
|
||||||
|
String? streetName,
|
||||||
|
) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
return ProductPageScreen(
|
||||||
|
configuration: ProductPageConfiguration(
|
||||||
|
// (REQUIRED): List of shops that should be displayed
|
||||||
|
// If there is only one, make a list with just one shop.
|
||||||
|
shops: Future.value(getCategories()),
|
||||||
|
|
||||||
|
pagePadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 4),
|
||||||
|
|
||||||
|
// (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: (ProductPageShop shop) =>
|
||||||
|
Future<ProductPageContent>.value(
|
||||||
|
getShopContent(shop.id),
|
||||||
|
),
|
||||||
|
|
||||||
|
// (REQUIRED): Function to navigate to the shopping cart
|
||||||
|
onNavigateToShoppingCart: () => onCompleteProductPage(context),
|
||||||
|
|
||||||
|
shopSelectorStyle: ShopSelectorStyle.row,
|
||||||
|
|
||||||
|
navigateToShoppingCartBuilder: (context) => const SizedBox.shrink(),
|
||||||
|
|
||||||
|
bottomNavigationBar: BottomNavigationBar(
|
||||||
|
fixedColor: theme.primaryColor,
|
||||||
|
unselectedItemColor: Colors.black,
|
||||||
|
type: BottomNavigationBarType.fixed,
|
||||||
|
items: const [
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
icon: Icon(Icons.home),
|
||||||
|
label: "Home",
|
||||||
|
),
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
icon: Icon(Icons.person_2_outlined),
|
||||||
|
label: "Profile",
|
||||||
|
),
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
icon: Icon(Icons.shopping_cart_outlined),
|
||||||
|
label: "Cart",
|
||||||
|
),
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
icon: Icon(Icons.menu),
|
||||||
|
label: "Menu",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
showSelectedLabels: false,
|
||||||
|
showUnselectedLabels: false,
|
||||||
|
onTap: (index) {
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
// context.go(homePage);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
context.go(FlutterShoppingPathRoutes.shoppingCart);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
productBuilder: (context, product) => Card(
|
||||||
|
elevation: 0,
|
||||||
|
color: const Color.fromARGB(255, 233, 233, 233),
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.zero,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Image.network(
|
||||||
|
product.imageUrl,
|
||||||
|
loadingBuilder: (context, child, loadingProgress) =>
|
||||||
|
loadingProgress == null
|
||||||
|
? child
|
||||||
|
: const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
errorBuilder: (context, error, stackTrace) =>
|
||||||
|
const Tooltip(
|
||||||
|
message: "Error loading image",
|
||||||
|
child: Icon(
|
||||||
|
Icons.error,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 5,
|
||||||
|
child: ColoredBox(
|
||||||
|
color: theme.scaffoldBackgroundColor,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
product.name,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
maxLines: 3,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"4.5",
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Icon(Icons.star, color: Colors.orange),
|
||||||
|
const Icon(Icons.star, color: Colors.orange),
|
||||||
|
const Icon(Icons.star, color: Colors.orange),
|
||||||
|
const Icon(Icons.star, color: Colors.orange),
|
||||||
|
const Icon(Icons.star_half,
|
||||||
|
color: Colors.orange),
|
||||||
|
Text(
|
||||||
|
"(3)",
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"\$${product.price.toStringAsFixed(2)}",
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Gratis bezorging door Amazon",
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
productService.addProduct(product as MyProduct);
|
||||||
|
},
|
||||||
|
child: const Text("In winkelwagen"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// (RECOMMENDED) The shop that is initially selected.
|
||||||
|
// Must be one of the shops in the [shops] list.
|
||||||
|
initialShopId: getCategories().first.id,
|
||||||
|
|
||||||
|
// (RECOMMENDED) Localizations for the product page.
|
||||||
|
localizations: const ProductPageLocalization(),
|
||||||
|
|
||||||
|
noContentBuilder: (context) => Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 128),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.warning,
|
||||||
|
size: 48,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Geen producten gevonden",
|
||||||
|
style: theme.textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// (OPTIONAL) Appbar
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const SizedBox(
|
||||||
|
height: 40,
|
||||||
|
child: SearchBar(
|
||||||
|
hintText: "Search products",
|
||||||
|
leading: Icon(
|
||||||
|
Icons.search,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
trailing: [
|
||||||
|
Icon(
|
||||||
|
Icons.fit_screen_outlined,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () {
|
||||||
|
context.go(homePage);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
bottom: AppBar(
|
||||||
|
backgroundColor: const Color.fromRGBO(203, 237, 230, 1),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.location_on_outlined),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
"Bestemming: ${streetName ?? "Mark - 1234AB Doetinchem Nederland"}",
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
primary: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// (OPTIONAL): Initial build shop id that overrides the initialShop
|
||||||
|
initialBuildShopId: initialBuildShopId,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
// (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) => ListTile(
|
||||||
|
title: Text(product.name),
|
||||||
|
subtitle: Text(product.price.toStringAsFixed(2)),
|
||||||
|
leading: Image.network(
|
||||||
|
product.imageUrl,
|
||||||
|
errorBuilder: (context, error, stackTrace) => const Tooltip(
|
||||||
|
message: "Error loading image",
|
||||||
|
child: Icon(
|
||||||
|
Icons.error,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.remove),
|
||||||
|
onPressed: () => productService.removeOneProduct(product),
|
||||||
|
),
|
||||||
|
Text("${product.quantity}"),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
onPressed: () => productService.addProduct(product),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// (OPTIONAL/REQUIRED) on confirm order callback:
|
||||||
|
// Either use this callback or the placeOrderButtonBuilder.
|
||||||
|
onConfirmOrder: (products) => onCompleteShoppingCart(context),
|
||||||
|
|
||||||
|
// (RECOMMENDED) localizations:
|
||||||
|
localizations: const ShoppingCartLocalizations(),
|
||||||
|
|
||||||
|
/// (OPTIONAL) no content builder for when there are no products
|
||||||
|
/// in the shopping cart.
|
||||||
|
noContentBuilder: (context) => const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 128),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.warning,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Geen producten in winkelmandje",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// (OPTIONAL) custom appbar:
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Shopping Cart"),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.arrow_back,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
context.go(FlutterShoppingPathRoutes.shop);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// (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 {
|
||||||
|
// return true;
|
||||||
|
// },
|
||||||
|
);
|
|
@ -0,0 +1,8 @@
|
||||||
|
import 'package:flutter_shopping/flutter_shopping.dart';
|
||||||
|
|
||||||
|
class MyCategory extends ProductPageShop {
|
||||||
|
const MyCategory({
|
||||||
|
required super.id,
|
||||||
|
required super.name,
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import 'package:flutter_shopping/flutter_shopping.dart';
|
||||||
|
|
||||||
|
class MyProduct extends ShoppingCartProduct with ProductPageProduct {
|
||||||
|
MyProduct({
|
||||||
|
required super.id,
|
||||||
|
required super.name,
|
||||||
|
required super.price,
|
||||||
|
required this.category,
|
||||||
|
required this.imageUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String category;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String imageUrl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final double? discountPrice = 0.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final bool hasDiscount = false;
|
||||||
|
}
|
30
packages/flutter_shopping/example_amazon/lib/src/routes.dart
Normal file
30
packages/flutter_shopping/example_amazon/lib/src/routes.dart
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import "package:amazon/src/configuration/shopping_configuration.dart";
|
||||||
|
import "package:amazon/src/ui/homepage.dart";
|
||||||
|
import "package:amazon/src/utils/go_router.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
import "package:go_router/go_router.dart";
|
||||||
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
|
||||||
|
const String homePage = "/";
|
||||||
|
|
||||||
|
final routerProvider = Provider<GoRouter>(
|
||||||
|
(ref) => GoRouter(
|
||||||
|
initialLocation: homePage,
|
||||||
|
routes: [
|
||||||
|
// Flutter Shopping Story Routes
|
||||||
|
...getShoppingStoryRoutes(
|
||||||
|
configuration: getFlutterShoppingConfiguration(),
|
||||||
|
),
|
||||||
|
// Home Route
|
||||||
|
GoRoute(
|
||||||
|
name: "home",
|
||||||
|
path: homePage,
|
||||||
|
pageBuilder: (context, state) => buildScreenWithFadeTransition(
|
||||||
|
context: context,
|
||||||
|
state: state,
|
||||||
|
child: const Homepage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
|
@ -0,0 +1,89 @@
|
||||||
|
import "package:amazon/src/models/my_category.dart";
|
||||||
|
import "package:amazon/src/models/my_product.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
|
||||||
|
Map<String, String> categories = {
|
||||||
|
"Electronics": "Electronica",
|
||||||
|
"Smart phones": "Telefoons",
|
||||||
|
"TV's": "TV's",
|
||||||
|
};
|
||||||
|
|
||||||
|
List<MyProduct> allProducts() => [
|
||||||
|
MyProduct(
|
||||||
|
id: "1",
|
||||||
|
name:
|
||||||
|
"Skar Audio Single 8\" Complete 1,200 Watt EVL Series Subwoofer Bass Package - Includes Loaded Enclosure with...",
|
||||||
|
price: 2.99,
|
||||||
|
category: categories["Electronics"]!,
|
||||||
|
imageUrl:
|
||||||
|
"https://m.media-amazon.com/images/I/710n3hnbfXL._AC_UY218_.jpg",
|
||||||
|
),
|
||||||
|
MyProduct(
|
||||||
|
id: "2",
|
||||||
|
name:
|
||||||
|
"Frameo 10.1 Inch WiFi Digital Picture Frame, 1280x800 HD IPS Touch Screen Photo Frame Electronic, 32GB Memory, Auto...",
|
||||||
|
price: 2.99,
|
||||||
|
category: categories["Electronics"]!,
|
||||||
|
imageUrl:
|
||||||
|
"https://m.media-amazon.com/images/I/61O+aorCp0L._AC_UY218_.jpg",
|
||||||
|
),
|
||||||
|
MyProduct(
|
||||||
|
id: "3",
|
||||||
|
name:
|
||||||
|
"STREBITO Electronics Precision Screwdriver Sets 142-Piece with 120 Bits Magnetic Repair Tool Kit for iPhone, MacBook,...",
|
||||||
|
price: 1.99,
|
||||||
|
category: categories["Electronics"]!,
|
||||||
|
imageUrl:
|
||||||
|
"https://m.media-amazon.com/images/I/81-C7lGtQsL._AC_UY218_.jpg",
|
||||||
|
),
|
||||||
|
MyProduct(
|
||||||
|
id: "4",
|
||||||
|
name:
|
||||||
|
"Samsung Galaxy A15 (SM-155M/DSN), 128GB 6GB RAM, Dual SIM, Factory Unlocked GSM, International Version (Wall...",
|
||||||
|
price: 1.99,
|
||||||
|
category: categories["Smart phones"]!,
|
||||||
|
imageUrl:
|
||||||
|
"https://m.media-amazon.com/images/I/51rp0nqaPoL._AC_UY218_.jpg",
|
||||||
|
),
|
||||||
|
MyProduct(
|
||||||
|
id: "5",
|
||||||
|
name:
|
||||||
|
"SAMSUNG Galaxy S24 Ultra Cell Phone, 512GB AI Smartphone, Unlocked Android, 50MP Zoom Camera, Long...",
|
||||||
|
price: 1.99,
|
||||||
|
category: categories["Smart phones"]!,
|
||||||
|
imageUrl:
|
||||||
|
"https://m.media-amazon.com/images/I/71ZoDT7a2wL._AC_UY218_.jpg",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
List<MyCategory> getCategories() => <MyCategory>[
|
||||||
|
MyCategory(id: "1", name: categories["Electronics"]!),
|
||||||
|
MyCategory(id: "2", name: categories["Smart phones"]!),
|
||||||
|
MyCategory(id: "3", name: categories["TV's"]!),
|
||||||
|
const MyCategory(id: "4", name: "Monitoren"),
|
||||||
|
const MyCategory(id: "5", name: "Speakers"),
|
||||||
|
const MyCategory(id: "6", name: "Toetsenborden"),
|
||||||
|
];
|
||||||
|
|
||||||
|
ProductPageContent getShopContent(String shopId) {
|
||||||
|
var products = getProducts(shopId);
|
||||||
|
return ProductPageContent(
|
||||||
|
products: products,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MyProduct> getProducts(String categoryId) {
|
||||||
|
if (categoryId == "1") {
|
||||||
|
return allProducts();
|
||||||
|
} else if (categoryId == "2") {
|
||||||
|
return allProducts()
|
||||||
|
.where((product) => product.category == categories["Smart phones"]!)
|
||||||
|
.toList();
|
||||||
|
} else if (categoryId == "3") {
|
||||||
|
return allProducts()
|
||||||
|
.where((product) => product.category == categories["TV's"]!)
|
||||||
|
.toList();
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
import "package:go_router/go_router.dart";
|
||||||
|
|
||||||
|
class Homepage extends StatelessWidget {
|
||||||
|
const Homepage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Badge(
|
||||||
|
label: const Text("1"),
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.shopping_cart_outlined, size: 50),
|
||||||
|
onPressed: () => context.go(FlutterShoppingPathRoutes.shop),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:go_router/go_router.dart";
|
||||||
|
|
||||||
|
CustomTransitionPage buildScreenWithFadeTransition<T>({
|
||||||
|
required BuildContext context,
|
||||||
|
required GoRouterState state,
|
||||||
|
required Widget child,
|
||||||
|
}) =>
|
||||||
|
CustomTransitionPage<T>(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: child,
|
||||||
|
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
|
||||||
|
FadeTransition(opacity: animation, child: child),
|
||||||
|
);
|
||||||
|
|
||||||
|
CustomTransitionPage buildScreenWithoutTransition<T>({
|
||||||
|
required BuildContext context,
|
||||||
|
required GoRouterState state,
|
||||||
|
required Widget child,
|
||||||
|
}) =>
|
||||||
|
CustomTransitionPage<T>(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: child,
|
||||||
|
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
|
||||||
|
child,
|
||||||
|
);
|
|
@ -0,0 +1,43 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
|
ThemeData getTheme() => ThemeData(
|
||||||
|
scaffoldBackgroundColor: const Color.fromRGBO(250, 249, 246, 1),
|
||||||
|
textTheme: const TextTheme(
|
||||||
|
labelMedium: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
titleMedium: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Color.fromRGBO(60, 60, 59, 1),
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
inputDecorationTheme: const InputDecorationTheme(
|
||||||
|
fillColor: Colors.white,
|
||||||
|
),
|
||||||
|
colorScheme: const ColorScheme.light(
|
||||||
|
primary: Color.fromRGBO(161, 203, 211, 1),
|
||||||
|
secondary: Color.fromRGBO(221, 235, 238, 1),
|
||||||
|
surface: Color.fromRGBO(255, 255, 255, 1),
|
||||||
|
),
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
backgroundColor: Color.fromRGBO(161, 220, 218, 1),
|
||||||
|
foregroundColor: Colors.black,
|
||||||
|
titleTextStyle: TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
filledButtonTheme: FilledButtonThemeData(
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: WidgetStateProperty.all(
|
||||||
|
Colors.yellow,
|
||||||
|
),
|
||||||
|
foregroundColor: WidgetStateProperty.all(
|
||||||
|
Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
28
packages/flutter_shopping/example_amazon/pubspec.yaml
Normal file
28
packages/flutter_shopping/example_amazon/pubspec.yaml
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
name: amazon
|
||||||
|
description: "A new Flutter project."
|
||||||
|
publish_to: 'none'
|
||||||
|
version: 1.0.0
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: '>=3.4.1 <4.0.0'
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_hooks: ^0.20.0
|
||||||
|
hooks_riverpod: ^2.1.1
|
||||||
|
go_router: 12.1.3
|
||||||
|
flutter_nested_categories:
|
||||||
|
git:
|
||||||
|
url: https://github.com/Iconica-Development/flutter_nested_categories
|
||||||
|
ref: 0.0.1
|
||||||
|
flutter_shopping:
|
||||||
|
path: ../
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_lints: ^3.0.0
|
||||||
|
|
||||||
|
flutter:
|
||||||
|
uses-material-design: true
|
|
@ -0,0 +1,11 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Iconica
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import "package:flutter_test/flutter_test.dart";
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test("", () {
|
||||||
|
expect(true, true);
|
||||||
|
});
|
||||||
|
}
|
|
@ -5,5 +5,7 @@ export "package:flutter_order_details/flutter_order_details.dart";
|
||||||
export "package:flutter_product_page/flutter_product_page.dart";
|
export "package:flutter_product_page/flutter_product_page.dart";
|
||||||
export "package:flutter_shopping_cart/flutter_shopping_cart.dart";
|
export "package:flutter_shopping_cart/flutter_shopping_cart.dart";
|
||||||
|
|
||||||
export "src/configuration/shopping_configuration.dart";
|
export "src/config/flutter_shopping_configuration.dart";
|
||||||
export "src/flutter_shopping_navigator_userstory.dart";
|
export "src/routes.dart";
|
||||||
|
export "src/user_stores/flutter_shopping_userstory_go_router.dart";
|
||||||
|
export "src/user_stores/flutter_shopping_userstory_navigation.dart";
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
import "package:go_router/go_router.dart";
|
||||||
|
|
||||||
|
/// Default order detail configuration for the app.
|
||||||
|
/// This configuration is used to create the order detail page.
|
||||||
|
OrderDetailConfiguration getDefaultOrderDetailConfiguration(
|
||||||
|
BuildContext context,
|
||||||
|
FlutterShoppingConfiguration configuration,
|
||||||
|
) =>
|
||||||
|
OrderDetailConfiguration(
|
||||||
|
steps: [
|
||||||
|
OrderDetailStep(
|
||||||
|
formKey: GlobalKey<FormState>(),
|
||||||
|
stepName: "Basic Information",
|
||||||
|
fields: [
|
||||||
|
OrderTextInput(
|
||||||
|
title: "First name",
|
||||||
|
outputKey: "first_name",
|
||||||
|
textController: TextEditingController(),
|
||||||
|
),
|
||||||
|
OrderTextInput(
|
||||||
|
title: "Last name",
|
||||||
|
outputKey: "last_name",
|
||||||
|
textController: TextEditingController(),
|
||||||
|
),
|
||||||
|
OrderEmailInput(
|
||||||
|
title: "Your email address",
|
||||||
|
outputKey: "email",
|
||||||
|
textController: TextEditingController(),
|
||||||
|
subtitle: "* We will send your order confirmation here",
|
||||||
|
hint: "your_email@mail.com",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
OrderDetailStep(
|
||||||
|
formKey: GlobalKey<FormState>(),
|
||||||
|
stepName: "Address Information",
|
||||||
|
fields: [
|
||||||
|
OrderAddressInput(
|
||||||
|
title: "Your address",
|
||||||
|
outputKey: "address",
|
||||||
|
textController: TextEditingController(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
OrderDetailStep(
|
||||||
|
formKey: GlobalKey<FormState>(),
|
||||||
|
stepName: "Payment Information",
|
||||||
|
fields: [
|
||||||
|
OrderChoiceInput(
|
||||||
|
title: "Payment option",
|
||||||
|
outputKey: "payment_option",
|
||||||
|
items: ["Pay now", "Pay later"],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onCompleted: (OrderResult result) async =>
|
||||||
|
onCompleteOrderDetails(context, configuration, result),
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Order Details"),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.close, color: Colors.white),
|
||||||
|
onPressed: () => context.go(FlutterShoppingPathRoutes.shoppingCart),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
|
@ -0,0 +1,43 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_order_details/flutter_order_details.dart";
|
||||||
|
|
||||||
|
/// Configuration class for the flutter_shopping user-story.
|
||||||
|
class FlutterShoppingConfiguration {
|
||||||
|
/// Constructor for the FlutterShoppingConfiguration.
|
||||||
|
const FlutterShoppingConfiguration({
|
||||||
|
required this.shopBuilder,
|
||||||
|
required this.shoppingCartBuilder,
|
||||||
|
required this.onCompleteUserStory,
|
||||||
|
this.orderDetailsBuilder,
|
||||||
|
this.onCompleteOrderDetails,
|
||||||
|
this.orderSuccessBuilder,
|
||||||
|
this.orderFailedBuilder,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Builder for the shop/product page.
|
||||||
|
final Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
String? initialBuildShopId,
|
||||||
|
String? streetName,
|
||||||
|
) shopBuilder;
|
||||||
|
|
||||||
|
/// Builder for the shopping cart page.
|
||||||
|
final Widget Function(BuildContext context) shoppingCartBuilder;
|
||||||
|
|
||||||
|
/// Function that is called when the user-story is completed.
|
||||||
|
final Function(BuildContext context) onCompleteUserStory;
|
||||||
|
|
||||||
|
/// Builder for the order details page. This does not have to be set if you
|
||||||
|
/// are using the default order details page.
|
||||||
|
final Widget Function(BuildContext context)? orderDetailsBuilder;
|
||||||
|
|
||||||
|
/// Allows you to execute actions before
|
||||||
|
final Future<bool> Function(BuildContext context, OrderResult result)?
|
||||||
|
onCompleteOrderDetails;
|
||||||
|
|
||||||
|
/// Builder for when the order is successful.
|
||||||
|
final Widget Function(BuildContext context)? orderSuccessBuilder;
|
||||||
|
|
||||||
|
/// Builder for when the order failed.
|
||||||
|
final Widget Function(BuildContext context)? orderFailedBuilder;
|
||||||
|
}
|
|
@ -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 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:flutter_shopping/flutter_shopping.dart";
|
|
||||||
import "package:flutter_shopping_local/flutter_shopping_local.dart";
|
|
||||||
|
|
||||||
/// User story for the shopping navigator.
|
|
||||||
class ShoppingNavigatorUserStory extends StatelessWidget {
|
|
||||||
/// Constructor for the shopping navigator user story.
|
|
||||||
const ShoppingNavigatorUserStory({
|
|
||||||
this.shoppingConfiguration,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Shopping configuration.
|
|
||||||
final ShoppingConfiguration? shoppingConfiguration;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => ShoppingProductPage(
|
|
||||||
shoppingConfiguration: shoppingConfiguration ??
|
|
||||||
ShoppingConfiguration(
|
|
||||||
shoppingService: LocalShoppingService(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shopping product page.
|
|
||||||
class ShoppingProductPage extends StatelessWidget {
|
|
||||||
/// Constructor for the shopping product page.
|
|
||||||
const ShoppingProductPage({
|
|
||||||
required this.shoppingConfiguration,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Shopping configuration.
|
|
||||||
final ShoppingConfiguration shoppingConfiguration;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var service = shoppingConfiguration.shoppingService;
|
|
||||||
return ProductPageScreen(
|
|
||||||
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
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var service = shoppingConfiguration.shoppingService.shoppingCartService;
|
|
||||||
return ShoppingCartScreen(
|
|
||||||
configuration: ShoppingCartConfig(
|
|
||||||
service: service,
|
|
||||||
productItemBuilder: shoppingConfiguration.productItemBuilder,
|
|
||||||
confirmOrderButtonBuilder:
|
|
||||||
shoppingConfiguration.confirmOrderButtonBuilder,
|
|
||||||
confirmOrderButtonHeight:
|
|
||||||
shoppingConfiguration.confirmOrderButtonHeight ?? 100,
|
|
||||||
sumBottomSheetBuilder: shoppingConfiguration.sumBottomSheetBuilder,
|
|
||||||
sumBottomSheetHeight: shoppingConfiguration.sumBottomSheetHeight ?? 100,
|
|
||||||
titleBuilder: shoppingConfiguration.titleBuilder,
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
28
packages/flutter_shopping/lib/src/go_router.dart
Normal file
28
packages/flutter_shopping/lib/src/go_router.dart
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:go_router/go_router.dart";
|
||||||
|
|
||||||
|
/// Builder with a fade transition for when navigating to a new screen.
|
||||||
|
CustomTransitionPage buildScreenWithFadeTransition<T>({
|
||||||
|
required BuildContext context,
|
||||||
|
required GoRouterState state,
|
||||||
|
required Widget child,
|
||||||
|
}) =>
|
||||||
|
CustomTransitionPage<T>(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: child,
|
||||||
|
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
|
||||||
|
FadeTransition(opacity: animation, child: child),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Builder without a transition for when navigating to a new screen.
|
||||||
|
CustomTransitionPage buildScreenWithoutTransition<T>({
|
||||||
|
required BuildContext context,
|
||||||
|
required GoRouterState state,
|
||||||
|
required Widget child,
|
||||||
|
}) =>
|
||||||
|
CustomTransitionPage<T>(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: child,
|
||||||
|
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
|
||||||
|
child,
|
||||||
|
);
|
35
packages/flutter_shopping/lib/src/routes.dart
Normal file
35
packages/flutter_shopping/lib/src/routes.dart
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
/// All the name routes used in the user-story.
|
||||||
|
mixin FlutterShoppingNameRoutes {
|
||||||
|
/// The shop name route.
|
||||||
|
static const String shop = "shop";
|
||||||
|
|
||||||
|
/// The shopping cart name route.
|
||||||
|
static const String shoppingCart = "shoppingcart";
|
||||||
|
|
||||||
|
/// The order details name route.
|
||||||
|
static const String orderDetails = "orderdetails";
|
||||||
|
|
||||||
|
/// The order success name route.
|
||||||
|
static const String orderSuccess = "ordersuccess";
|
||||||
|
|
||||||
|
/// The order failed name route.
|
||||||
|
static const String orderFailed = "orderfailed";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All the path routes used in the user-story.
|
||||||
|
mixin FlutterShoppingPathRoutes {
|
||||||
|
/// The shop page route.
|
||||||
|
static const String shop = "/shop";
|
||||||
|
|
||||||
|
/// The shopping cart page route.
|
||||||
|
static const String shoppingCart = "/shopping-cart";
|
||||||
|
|
||||||
|
/// The order details page route.
|
||||||
|
static const String orderDetails = "/order-details";
|
||||||
|
|
||||||
|
/// The order success page route.
|
||||||
|
static const String orderSuccess = "/order-success";
|
||||||
|
|
||||||
|
/// The order failed page route.
|
||||||
|
static const String orderFailed = "/order-failed";
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
import "package:flutter_shopping/src/config/default_order_detail_configuration.dart";
|
||||||
|
import "package:flutter_shopping/src/widgets/default_order_failed_widget.dart";
|
||||||
|
import "package:flutter_shopping/src/widgets/default_order_succes_widget.dart";
|
||||||
|
import "package:go_router/go_router.dart";
|
||||||
|
|
||||||
|
/// All the routes for the shopping story.
|
||||||
|
List<GoRoute> getShoppingStoryRoutes({
|
||||||
|
required FlutterShoppingConfiguration configuration,
|
||||||
|
}) =>
|
||||||
|
<GoRoute>[
|
||||||
|
GoRoute(
|
||||||
|
name: FlutterShoppingNameRoutes.shop,
|
||||||
|
path: FlutterShoppingPathRoutes.shop,
|
||||||
|
builder: (context, state) => configuration.shopBuilder(
|
||||||
|
context,
|
||||||
|
state.uri.queryParameters["id"],
|
||||||
|
state.uri.queryParameters["street"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: FlutterShoppingNameRoutes.shoppingCart,
|
||||||
|
path: FlutterShoppingPathRoutes.shoppingCart,
|
||||||
|
builder: (context, state) => configuration.shoppingCartBuilder(context),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: FlutterShoppingNameRoutes.orderDetails,
|
||||||
|
path: FlutterShoppingPathRoutes.orderDetails,
|
||||||
|
builder: (context, state) => configuration.orderDetailsBuilder != null
|
||||||
|
? configuration.orderDetailsBuilder!(context)
|
||||||
|
: OrderDetailScreen(
|
||||||
|
configuration:
|
||||||
|
getDefaultOrderDetailConfiguration(context, configuration),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: FlutterShoppingNameRoutes.orderSuccess,
|
||||||
|
path: FlutterShoppingPathRoutes.orderSuccess,
|
||||||
|
builder: (context, state) => configuration.orderSuccessBuilder != null
|
||||||
|
? configuration.orderSuccessBuilder!(context)
|
||||||
|
: DefaultOrderSucces(configuration: configuration),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: FlutterShoppingNameRoutes.orderFailed,
|
||||||
|
path: FlutterShoppingPathRoutes.orderFailed,
|
||||||
|
builder: (context, state) => configuration.orderFailedBuilder != null
|
||||||
|
? configuration.orderFailedBuilder!(context)
|
||||||
|
: DefaultOrderFailed(configuration: configuration),
|
||||||
|
),
|
||||||
|
];
|
|
@ -0,0 +1,52 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
import "package:go_router/go_router.dart";
|
||||||
|
|
||||||
|
/// Default on complete order details function.
|
||||||
|
/// This function will navigate to the order success or order failed page.
|
||||||
|
///
|
||||||
|
/// You can create your own implementation if you decide to use a different
|
||||||
|
/// approach.
|
||||||
|
Future<void> onCompleteOrderDetails(
|
||||||
|
BuildContext context,
|
||||||
|
FlutterShoppingConfiguration configuration,
|
||||||
|
OrderResult result,
|
||||||
|
) async {
|
||||||
|
var go = context.go;
|
||||||
|
var succesful = true;
|
||||||
|
|
||||||
|
if (configuration.onCompleteOrderDetails != null) {
|
||||||
|
var executionResult =
|
||||||
|
await configuration.onCompleteOrderDetails?.call(context, result);
|
||||||
|
|
||||||
|
if (executionResult == null || !executionResult) {
|
||||||
|
succesful = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (succesful) {
|
||||||
|
go(FlutterShoppingPathRoutes.orderSuccess);
|
||||||
|
} else {
|
||||||
|
go(FlutterShoppingPathRoutes.orderFailed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default on complete shopping cart function.
|
||||||
|
///
|
||||||
|
/// You can create your own implementation if you decide to use a different
|
||||||
|
/// approach.
|
||||||
|
void onCompleteShoppingCart(
|
||||||
|
BuildContext context,
|
||||||
|
) {
|
||||||
|
context.go(FlutterShoppingPathRoutes.orderDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default on complete product page function.
|
||||||
|
///
|
||||||
|
/// You can create your own implementation if you decide to use a different
|
||||||
|
/// approach.
|
||||||
|
void onCompleteProductPage(
|
||||||
|
BuildContext context,
|
||||||
|
) {
|
||||||
|
context.go(FlutterShoppingPathRoutes.shoppingCart);
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
|
||||||
|
/// Default order failed widget.
|
||||||
|
class DefaultOrderFailed extends StatelessWidget {
|
||||||
|
/// Constructor for the DefaultOrderFailed.
|
||||||
|
const DefaultOrderFailed({
|
||||||
|
required this.configuration,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Configuration for the user-story.
|
||||||
|
final FlutterShoppingConfiguration configuration;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
var finishOrderButton = FilledButton(
|
||||||
|
onPressed: () => configuration.onCompleteUserStory(context),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 32.0,
|
||||||
|
vertical: 8.0,
|
||||||
|
),
|
||||||
|
child: Text("Go back".toUpperCase()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
var content = Column(
|
||||||
|
children: [
|
||||||
|
const Spacer(),
|
||||||
|
const Icon(
|
||||||
|
Icons.error,
|
||||||
|
size: 100,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
"Uh oh.",
|
||||||
|
style: theme.textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
Text(
|
||||||
|
"It seems that something went wrong.",
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Please try again later.",
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
finishOrderButton,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: SafeArea(
|
||||||
|
child: Center(
|
||||||
|
child: content,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_shopping/flutter_shopping.dart";
|
||||||
|
|
||||||
|
/// Default order success widget.
|
||||||
|
class DefaultOrderSucces extends StatelessWidget {
|
||||||
|
/// Constructor for the DefaultOrderSucces.
|
||||||
|
const DefaultOrderSucces({
|
||||||
|
required this.configuration,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Configuration for the user-story.
|
||||||
|
final FlutterShoppingConfiguration configuration;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
var finishOrderButton = FilledButton(
|
||||||
|
onPressed: () => configuration.onCompleteUserStory(context),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 32.0,
|
||||||
|
vertical: 8.0,
|
||||||
|
),
|
||||||
|
child: Text("Finish Order".toUpperCase()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
var content = Column(
|
||||||
|
children: [
|
||||||
|
const Spacer(),
|
||||||
|
Text("#123456", style: theme.textTheme.titleLarge),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
"Order Succesfully Placed!",
|
||||||
|
style: theme.textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Thank you for your order!",
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
"Your order will be delivered soon.",
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
"Do you want to order again?",
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
finishOrderButton,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: SafeArea(
|
||||||
|
child: Center(
|
||||||
|
child: content,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
name: flutter_shopping
|
name: flutter_shopping
|
||||||
description: "A new Flutter project."
|
description: "A new Flutter project."
|
||||||
publish_to: "none"
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
version: 2.0.0
|
version: 1.0.0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.3.4 <4.0.0"
|
sdk: '>=3.3.4 <4.0.0'
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
|
@ -13,28 +13,18 @@ dependencies:
|
||||||
flutter_product_page:
|
flutter_product_page:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_shopping
|
url: https://github.com/Iconica-Development/flutter_shopping
|
||||||
ref: 2.0.0
|
ref: 1.0.0
|
||||||
path: packages/flutter_product_page
|
path: packages/flutter_product_page
|
||||||
flutter_shopping_cart:
|
flutter_shopping_cart:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_shopping
|
url: https://github.com/Iconica-Development/flutter_shopping
|
||||||
ref: 2.0.0
|
ref: 1.0.0
|
||||||
path: packages/flutter_shopping_cart
|
path: packages/flutter_shopping_cart
|
||||||
flutter_order_details:
|
flutter_order_details:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_shopping
|
url: https://github.com/Iconica-Development/flutter_shopping
|
||||||
ref: 2.0.0
|
ref: 1.0.0
|
||||||
path: packages/flutter_order_details
|
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:
|
flutter_test:
|
||||||
|
@ -45,3 +35,13 @@ dev_dependencies:
|
||||||
ref: 7.0.0
|
ref: 7.0.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
|
# assets:
|
||||||
|
# - images/a_dot_burr.jpeg
|
||||||
|
# - images/a_dot_ham.jpeg
|
||||||
|
|
||||||
|
# fonts:
|
||||||
|
# - family: Schyler
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/Schyler-Regular.ttf
|
||||||
|
# - asset: fonts/Schyler-Italic.ttf
|
||||||
|
# style: italic
|
||||||
|
|
|
@ -2,5 +2,7 @@
|
||||||
library flutter_shopping_cart;
|
library flutter_shopping_cart;
|
||||||
|
|
||||||
export "src/config/shopping_cart_config.dart";
|
export "src/config/shopping_cart_config.dart";
|
||||||
export "src/config/shopping_cart_translations.dart";
|
export "src/config/shopping_cart_localizations.dart";
|
||||||
|
export "src/models/shopping_cart_product.dart";
|
||||||
|
export "src/services/product_service.dart";
|
||||||
export "src/widgets/shopping_cart_screen.dart";
|
export "src/widgets/shopping_cart_screen.dart";
|
||||||
|
|
|
@ -1,89 +1,133 @@
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
|
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
|
||||||
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
|
|
||||||
|
Widget _defaultNoContentBuilder(BuildContext context) =>
|
||||||
|
const SizedBox.shrink();
|
||||||
|
|
||||||
/// Shopping cart configuration
|
/// Shopping cart configuration
|
||||||
///
|
///
|
||||||
/// This class is used to configure the shopping cart.
|
/// This class is used to configure the shopping cart.
|
||||||
class ShoppingCartConfig {
|
class ShoppingCartConfig<T extends ShoppingCartProduct> {
|
||||||
/// Creates a shopping cart configuration.
|
/// Creates a shopping cart configuration.
|
||||||
ShoppingCartConfig({
|
ShoppingCartConfig({
|
||||||
required this.service,
|
required this.productService,
|
||||||
required this.onConfirmOrder,
|
//
|
||||||
this.productItemBuilder,
|
this.onConfirmOrder,
|
||||||
this.confirmOrderButtonBuilder,
|
this.confirmOrderButtonBuilder,
|
||||||
this.confirmOrderButtonHeight = 100,
|
this.confirmOrderButtonHeight = 100,
|
||||||
|
//
|
||||||
this.sumBottomSheetBuilder,
|
this.sumBottomSheetBuilder,
|
||||||
this.sumBottomSheetHeight = 100,
|
this.sumBottomSheetHeight = 100,
|
||||||
|
//
|
||||||
|
this.title,
|
||||||
this.titleBuilder,
|
this.titleBuilder,
|
||||||
this.translations = const ShoppingCartTranslations(),
|
//
|
||||||
this.pagePadding = const EdgeInsets.symmetric(horizontal: 32),
|
this.localizations = const ShoppingCartLocalizations(),
|
||||||
|
//
|
||||||
|
this.padding = const EdgeInsets.symmetric(horizontal: 32),
|
||||||
this.bottomPadding = const EdgeInsets.fromLTRB(44, 0, 44, 32),
|
this.bottomPadding = const EdgeInsets.fromLTRB(44, 0, 44, 32),
|
||||||
this.appBarBuilder,
|
//
|
||||||
});
|
this.appBar,
|
||||||
|
//
|
||||||
|
Widget Function(BuildContext context, Locale locale, T product)?
|
||||||
|
productItemBuilder,
|
||||||
|
Widget Function(BuildContext context) noContentBuilder =
|
||||||
|
_defaultNoContentBuilder,
|
||||||
|
}) : assert(
|
||||||
|
confirmOrderButtonBuilder != null || onConfirmOrder != null,
|
||||||
|
"""
|
||||||
|
If you override the confirm order button builder,
|
||||||
|
you cannot use the onConfirmOrder callback.""",
|
||||||
|
),
|
||||||
|
assert(
|
||||||
|
confirmOrderButtonBuilder == null || onConfirmOrder == null,
|
||||||
|
"""
|
||||||
|
If you do not override the confirm order button builder,
|
||||||
|
you must use the onConfirmOrder callback.""",
|
||||||
|
),
|
||||||
|
_noContentBuilder = noContentBuilder {
|
||||||
|
_productItemBuilder = productItemBuilder;
|
||||||
|
_productItemBuilder ??= (context, locale, product) => ListTile(
|
||||||
|
title: Text(product.name),
|
||||||
|
subtitle: Text(product.price.toString()),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
onPressed: () => productService.removeProduct(product),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Product service. The product service is used to manage the products in the
|
/// Product Service. The service contains all the products that
|
||||||
/// shopping cart.
|
/// a shopping cart can contain. Each product must extend the [Product] class.
|
||||||
final ShoppingCartService service;
|
/// The service is used to add, remove, and update products.
|
||||||
|
///
|
||||||
|
/// The service can be seperate for each shopping cart in-case you want to
|
||||||
|
/// support seperate shopping carts for shop.
|
||||||
|
ProductService<T> productService = ProductService<T>(<T>[]);
|
||||||
|
|
||||||
|
late final Widget Function(BuildContext context, Locale locale, T product)?
|
||||||
|
_productItemBuilder;
|
||||||
|
|
||||||
/// Product item builder. This builder is used to build the product item
|
/// Product item builder. This builder is used to build the product item
|
||||||
/// that will be displayed in the shopping cart.
|
/// that will be displayed in the shopping cart.
|
||||||
Widget Function(
|
Widget Function(BuildContext context, Locale locale, T product)
|
||||||
BuildContext context,
|
get productItemBuilder => _productItemBuilder!;
|
||||||
Product product,
|
|
||||||
ShoppingCartConfig configuration,
|
final Widget Function(BuildContext context) _noContentBuilder;
|
||||||
)? productItemBuilder;
|
|
||||||
|
/// No content builder. This builder is used to build the no content widget
|
||||||
|
/// that will be displayed in the shopping cart when there are no products.
|
||||||
|
Widget Function(BuildContext context) get noContentBuilder =>
|
||||||
|
_noContentBuilder;
|
||||||
|
|
||||||
/// Confirm order button builder. This builder is used to build the confirm
|
/// Confirm order button builder. This builder is used to build the confirm
|
||||||
/// order button that will be displayed in the shopping cart.
|
/// order button that will be displayed in the shopping cart.
|
||||||
/// If you override this builder, you cannot use the [onConfirmOrder] callback
|
/// If you override this builder, you cannot use the [onConfirmOrder] callback
|
||||||
Widget Function(
|
final Widget Function(BuildContext context)? confirmOrderButtonBuilder;
|
||||||
BuildContext context,
|
|
||||||
ShoppingCartConfig configuration,
|
|
||||||
Function(List<Product> products) onConfirmOrder,
|
|
||||||
)? confirmOrderButtonBuilder;
|
|
||||||
|
|
||||||
/// Confirm order button height. The height of the confirm order button.
|
/// Confirm order button height. The height of the confirm order button.
|
||||||
/// This height is used to calculate the bottom padding of the shopping cart.
|
/// 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
|
/// If you override the confirm order button builder, you must provide a
|
||||||
/// height.
|
/// height.
|
||||||
double confirmOrderButtonHeight;
|
final double confirmOrderButtonHeight;
|
||||||
|
|
||||||
/// Confirm order callback. This callback is called when the confirm order
|
/// Confirm order callback. This callback is called when the confirm order
|
||||||
/// button is pressed. The callback will not be called if you override the
|
/// button is pressed. The callback will not be called if you override the
|
||||||
/// confirm order button builder.
|
/// confirm order button builder.
|
||||||
final Function(List<Product> products) onConfirmOrder;
|
final Function(List<T> products)? onConfirmOrder;
|
||||||
|
|
||||||
/// Sum bottom sheet builder. This builder is used to build the sum bottom
|
/// 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
|
/// 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.
|
/// can be used to display the total sum of the products in the shopping cart.
|
||||||
Widget Function(BuildContext context, ShoppingCartConfig configuration)?
|
final Widget Function(BuildContext context)? sumBottomSheetBuilder;
|
||||||
sumBottomSheetBuilder;
|
|
||||||
|
|
||||||
/// Sum bottom sheet height. The height of the sum bottom sheet.
|
/// Sum bottom sheet height. The height of the sum bottom sheet.
|
||||||
/// This height is used to calculate the bottom padding of the shopping cart.
|
/// 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.
|
/// If you override the sum bottom sheet builder, you must provide a height.
|
||||||
double sumBottomSheetHeight;
|
final double sumBottomSheetHeight;
|
||||||
|
|
||||||
/// Padding around the shopping cart. The padding is used to create space
|
/// Padding around the shopping cart. The padding is used to create space
|
||||||
/// around the shopping cart.
|
/// around the shopping cart.
|
||||||
EdgeInsets pagePadding;
|
final EdgeInsets padding;
|
||||||
|
|
||||||
/// Bottom padding of the shopping cart. The bottom padding is used to create
|
/// 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
|
/// a padding around the bottom sheet. This padding is ignored when the
|
||||||
/// [sumBottomSheetBuilder] is overridden.
|
/// [sumBottomSheetBuilder] is overridden.
|
||||||
EdgeInsets bottomPadding;
|
final EdgeInsets bottomPadding;
|
||||||
|
|
||||||
/// Title builder. This builder is used to
|
/// Title of the shopping cart. The title is displayed at the top of the
|
||||||
/// build the title of the shopping cart.
|
/// shopping cart. If you provide a title builder, the title will be ignored.
|
||||||
final Widget Function(
|
final String? title;
|
||||||
BuildContext context,
|
|
||||||
String title,
|
|
||||||
)? titleBuilder;
|
|
||||||
|
|
||||||
/// Shopping cart translations. The translations for the shopping cart.
|
/// Title builder. This builder is used to build the title of the shopping
|
||||||
ShoppingCartTranslations translations;
|
/// cart. The title is displayed at the top of the shopping cart. If you
|
||||||
|
/// use the title builder, the [title] will be ignored.
|
||||||
|
final Widget Function(BuildContext context)? titleBuilder;
|
||||||
|
|
||||||
|
/// Shopping cart localizations. The localizations are used to localize the
|
||||||
|
/// shopping cart.
|
||||||
|
final ShoppingCartLocalizations localizations;
|
||||||
|
|
||||||
/// App bar for the shopping cart screen.
|
/// App bar for the shopping cart screen.
|
||||||
PreferredSizeWidget Function(BuildContext context)? appBarBuilder;
|
final PreferredSizeWidget? appBar;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
|
/// Shopping cart localizations
|
||||||
|
class ShoppingCartLocalizations {
|
||||||
|
/// Creates shopping cart localizations
|
||||||
|
const ShoppingCartLocalizations({
|
||||||
|
this.locale = const Locale("en", "US"),
|
||||||
|
this.placeOrder = "PLACE ORDER",
|
||||||
|
this.sum = "Total:",
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Locale for the shopping cart.
|
||||||
|
/// This locale will be used to format the currency.
|
||||||
|
/// Default is English.
|
||||||
|
final Locale locale;
|
||||||
|
|
||||||
|
/// Localization for the place order button.
|
||||||
|
/// This text will only be displayed if you're not using the place order
|
||||||
|
/// button builder.
|
||||||
|
final String placeOrder;
|
||||||
|
|
||||||
|
/// Localization for the sum.
|
||||||
|
final String sum;
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
/// Abstract class for Product
|
||||||
|
///
|
||||||
|
/// All products that want to be added to the shopping cart
|
||||||
|
/// must extend this class.
|
||||||
|
abstract class ShoppingCartProduct {
|
||||||
|
/// Creates a new product.
|
||||||
|
ShoppingCartProduct({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.price,
|
||||||
|
this.quantity = 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Unique product identifier.
|
||||||
|
/// This identifier will be used to identify the product in the shopping cart.
|
||||||
|
/// If you don't provide an identifier, a random identifier will be generated.
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
/// Product name.
|
||||||
|
/// This name will be displayed in the shopping cart.
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
/// Product price.
|
||||||
|
/// This price will be displayed in the shopping cart.
|
||||||
|
final double price;
|
||||||
|
|
||||||
|
/// Quantity for the product.
|
||||||
|
int quantity;
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
import "package:flutter/foundation.dart";
|
||||||
|
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
|
||||||
|
|
||||||
|
/// Product service. This class is responsible for managing the products.
|
||||||
|
/// The service is used to add, remove, and update products.
|
||||||
|
class ProductService<T extends ShoppingCartProduct> extends ChangeNotifier {
|
||||||
|
/// Creates a product service.
|
||||||
|
ProductService(this.products);
|
||||||
|
|
||||||
|
/// List of products in the shopping cart.
|
||||||
|
final List<T> products;
|
||||||
|
|
||||||
|
/// Adds a product to the shopping cart.
|
||||||
|
void addProduct(T product) {
|
||||||
|
for (var p in products) {
|
||||||
|
if (p.id == product.id) {
|
||||||
|
p.quantity++;
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
products.add(product);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a product from the shopping cart.
|
||||||
|
void removeProduct(T product) {
|
||||||
|
for (var p in products) {
|
||||||
|
if (p.id == product.id) {
|
||||||
|
products.remove(p);
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes one product from the shopping cart.
|
||||||
|
void removeOneProduct(T product) {
|
||||||
|
for (var p in products) {
|
||||||
|
if (p.id == product.id) {
|
||||||
|
if (p.quantity > 1) {
|
||||||
|
p.quantity--;
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
products.remove(product);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Counts the number of products in the shopping cart.
|
||||||
|
int countProducts() {
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
for (var product in products) {
|
||||||
|
count += product.quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Empties the shopping cart.
|
||||||
|
void clear() {
|
||||||
|
products.clear();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue