mirror of
https://github.com/Iconica-Development/flutter_shopping.git
synced 2025-05-18 16:33:45 +02:00
fix: design changes (#9)
* fix: shopping cart screen * fix: product page * fix: add popup to shopping cart * fix: order detail screens * fix: button styling * fix: dependency --------- Co-authored-by: mike doornenbal <mikedoornenbal9@icloud.com>
This commit is contained in:
parent
0838b7b017
commit
5a24f7cf6f
54 changed files with 1445 additions and 2421 deletions
|
@ -3,15 +3,6 @@ library flutter_order_details;
|
|||
|
||||
export "src/configuration/order_detail_configuration.dart";
|
||||
export "src/configuration/order_detail_localization.dart";
|
||||
export "src/configuration/order_detail_step.dart";
|
||||
export "src/configuration/order_detail_title_style.dart";
|
||||
export "src/models/order_address_input.dart";
|
||||
export "src/models/order_choice_input.dart";
|
||||
export "src/models/order_dropdown_input.dart";
|
||||
export "src/models/order_email_input.dart";
|
||||
export "src/models/order_input.dart";
|
||||
export "src/models/order_phone_input.dart";
|
||||
export "src/models/order_result.dart";
|
||||
export "src/models/order_text_input.dart";
|
||||
export "src/models/order_time_picker_input.dart";
|
||||
export "src/widgets/order_detail_screen.dart";
|
||||
|
|
|
@ -1,50 +1,537 @@
|
|||
import "package:flutter/widgets.dart";
|
||||
import "package:flutter_order_details/src/configuration/order_detail_localization.dart";
|
||||
import "package:flutter_order_details/src/configuration/order_detail_step.dart";
|
||||
import "package:flutter_order_details/src/models/order_result.dart";
|
||||
// ignore_for_file: avoid_annotating_with_dynamic
|
||||
|
||||
import "package:animated_toggle/animated_toggle.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_form_wizard/flutter_form.dart";
|
||||
import "package:flutter_order_details/flutter_order_details.dart";
|
||||
|
||||
/// Configuration for the order detail screen.
|
||||
class OrderDetailConfiguration {
|
||||
/// Constructor for the order detail configuration.
|
||||
const OrderDetailConfiguration({
|
||||
required this.steps,
|
||||
//
|
||||
OrderDetailConfiguration({
|
||||
required this.onCompleted,
|
||||
//
|
||||
this.progressIndicator = true,
|
||||
//
|
||||
this.pages = _defaultPages,
|
||||
this.localization = const OrderDetailLocalization(),
|
||||
//
|
||||
this.inputFieldPadding = const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 16,
|
||||
),
|
||||
this.titlePadding = const EdgeInsets.only(left: 16, right: 16, top: 16),
|
||||
//
|
||||
this.appBar,
|
||||
this.appBar = _defaultAppBar,
|
||||
this.nextbuttonBuilder = _defaultNextButtonBuilder,
|
||||
});
|
||||
|
||||
/// The different steps that the user has to go through to complete the order.
|
||||
/// Each step contains a list of fields that the user has to fill in.
|
||||
final List<OrderDetailStep> steps;
|
||||
final List<FlutterFormPage> Function(BuildContext context) pages;
|
||||
|
||||
/// Callback function that is called when the user has completed the order.
|
||||
/// The result of the order is passed as an argument to the function.
|
||||
final Function(OrderResult result) onCompleted;
|
||||
|
||||
/// Whether or not you want to show a progress indicator at
|
||||
/// the top of the screen.
|
||||
final bool progressIndicator;
|
||||
final Function(dynamic value) onCompleted;
|
||||
|
||||
/// Localization for the order detail screen.
|
||||
final OrderDetailLocalization localization;
|
||||
|
||||
/// Padding around the input fields.
|
||||
final EdgeInsets inputFieldPadding;
|
||||
|
||||
/// Padding around the title of the input fields.
|
||||
final EdgeInsets titlePadding;
|
||||
|
||||
/// Optional app bar that you can pass to the order detail screen.
|
||||
final PreferredSizeWidget? appBar;
|
||||
final AppBar Function(
|
||||
BuildContext context,
|
||||
OrderDetailLocalization localizations,
|
||||
) appBar;
|
||||
|
||||
/// Optional next button builder that you can pass to the order detail screen.
|
||||
final Widget Function(
|
||||
int a,
|
||||
// ignore: avoid_positional_boolean_parameters
|
||||
bool b,
|
||||
BuildContext context,
|
||||
OrderDetailConfiguration configuration,
|
||||
FlutterFormController controller,
|
||||
) nextbuttonBuilder;
|
||||
}
|
||||
|
||||
AppBar _defaultAppBar(
|
||||
BuildContext context,
|
||||
OrderDetailLocalization localizations,
|
||||
) {
|
||||
var theme = Theme.of(context);
|
||||
return AppBar(
|
||||
title: Text(
|
||||
localizations.orderDetailsTitle,
|
||||
style: theme.textTheme.headlineLarge,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _defaultNextButtonBuilder(
|
||||
int currentStep,
|
||||
bool b,
|
||||
BuildContext context,
|
||||
OrderDetailConfiguration configuration,
|
||||
FlutterFormController controller,
|
||||
) {
|
||||
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 {
|
||||
controller.validateAndSaveCurrentStep();
|
||||
await controller.autoNextStep();
|
||||
},
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<FlutterFormPage> _defaultPages(BuildContext context) {
|
||||
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;
|
||||
},
|
||||
childLeft: Center(
|
||||
child: ListenableBuilder(
|
||||
listenable: switchStatus,
|
||||
builder: (context, widget) => Text(
|
||||
"Morning",
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
color: switchStatus.value
|
||||
? theme.colorScheme.primary
|
||||
: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
childRight: Center(
|
||||
child: ListenableBuilder(
|
||||
listenable: switchStatus,
|
||||
builder: (context, widget) => Text(
|
||||
"Afternoon",
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
color: switchStatus.value
|
||||
? Colors.white
|
||||
: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
ListenableBuilder(
|
||||
listenable: switchStatus,
|
||||
builder: (context, widget) => 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",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
|
|
@ -2,17 +2,17 @@
|
|||
class OrderDetailLocalization {
|
||||
/// Constructor for the order detail localization.
|
||||
const OrderDetailLocalization({
|
||||
this.nextButton = "Next",
|
||||
this.backButton = "Back",
|
||||
this.nextButton = "Order",
|
||||
this.completeButton = "Complete",
|
||||
this.orderDetailsTitle = "Information",
|
||||
});
|
||||
|
||||
/// Next button localization.
|
||||
final String nextButton;
|
||||
|
||||
/// Back button localization.
|
||||
final String backButton;
|
||||
|
||||
/// Complete button localization.
|
||||
final String completeButton;
|
||||
|
||||
/// Title for the order details page.
|
||||
final String orderDetailsTitle;
|
||||
}
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,160 +0,0 @@
|
|||
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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,175 +0,0 @@
|
|||
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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,153 +0,0 @@
|
|||
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,
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,173 +0,0 @@
|
|||
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,
|
||||
);
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,353 +0,0 @@
|
|||
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,4 +1,5 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_form_wizard/flutter_form.dart";
|
||||
import "package:flutter_order_details/flutter_order_details.dart";
|
||||
|
||||
/// Order Detail Screen.
|
||||
|
@ -17,257 +18,29 @@ class OrderDetailScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
var controller = FlutterFormController();
|
||||
return Scaffold(
|
||||
appBar: widget.configuration.appBar,
|
||||
body: Stack(
|
||||
children: [
|
||||
pageBody,
|
||||
if (_blurBackground) pageBlur,
|
||||
],
|
||||
appBar: widget.configuration.appBar
|
||||
.call(context, widget.configuration.localization),
|
||||
body: FlutterForm(
|
||||
formController: controller,
|
||||
options: FlutterFormOptions(
|
||||
nextButton: (a, b) => widget.configuration.nextbuttonBuilder(
|
||||
a,
|
||||
b,
|
||||
context,
|
||||
widget.configuration,
|
||||
controller,
|
||||
),
|
||||
pages: widget.configuration.pages.call(context),
|
||||
onFinished: (data) {
|
||||
widget.configuration.onCompleted.call(data);
|
||||
},
|
||||
onNext: (step, data) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,6 +1,7 @@
|
|||
name: flutter_order_details
|
||||
description: "A Flutter module for order details."
|
||||
version: 1.0.0
|
||||
version: 2.0.0
|
||||
publish_to: 'none'
|
||||
|
||||
environment:
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
|
@ -8,6 +9,14 @@ environment:
|
|||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
animated_toggle:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_animated_toggle
|
||||
ref: 0.0.3
|
||||
flutter_form_wizard:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_form_wizard
|
||||
ref: 6.5.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -18,13 +27,4 @@ dev_dependencies:
|
|||
ref: 7.0.0
|
||||
|
||||
flutter:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
|
|
|
@ -7,7 +7,6 @@ export "src/configuration/product_page_configuration.dart";
|
|||
export "src/configuration/product_page_content.dart";
|
||||
export "src/configuration/product_page_localization.dart";
|
||||
export "src/configuration/product_page_shop_selector_style.dart";
|
||||
export "src/models/product.dart";
|
||||
export "src/models/product_page_shop.dart";
|
||||
export "src/ui/product_page.dart";
|
||||
export "src/ui/product_page_screen.dart";
|
||||
|
|
|
@ -1,47 +1,38 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_product_page/flutter_product_page.dart";
|
||||
import "package:flutter_product_page/src/services/shopping_cart_notifier.dart";
|
||||
import "package:flutter_product_page/src/ui/widgets/product_item_popup.dart";
|
||||
import "package:flutter_shopping/flutter_shopping.dart";
|
||||
|
||||
/// Configuration for the product page.
|
||||
class ProductPageConfiguration {
|
||||
/// Constructor for the product page configuration.
|
||||
ProductPageConfiguration({
|
||||
required this.shops,
|
||||
//
|
||||
required this.getProducts,
|
||||
//
|
||||
required this.onAddToCart,
|
||||
required this.onNavigateToShoppingCart,
|
||||
this.navigateToShoppingCartBuilder,
|
||||
//
|
||||
this.navigateToShoppingCartBuilder = _defaultNavigateToShoppingCartBuilder,
|
||||
this.initialShopId,
|
||||
//
|
||||
this.productBuilder,
|
||||
//
|
||||
this.onShopSelectionChange,
|
||||
this.getProductsInShoppingCart,
|
||||
//
|
||||
this.localizations = const ProductPageLocalization(),
|
||||
//
|
||||
this.shopSelectorStyle = ShopSelectorStyle.spacedWrap,
|
||||
this.categoryStylingConfiguration =
|
||||
const ProductPageCategoryStylingConfiguration(),
|
||||
//
|
||||
this.pagePadding = const EdgeInsets.all(4),
|
||||
//
|
||||
this.appBar,
|
||||
this.appBar = _defaultAppBar,
|
||||
this.bottomNavigationBar,
|
||||
//
|
||||
Function(
|
||||
BuildContext context,
|
||||
ProductPageProduct product,
|
||||
Product product,
|
||||
)? onProductDetail,
|
||||
String Function(
|
||||
ProductPageProduct product,
|
||||
Product product,
|
||||
)? getDiscountDescription,
|
||||
Widget Function(
|
||||
BuildContext context,
|
||||
ProductPageProduct product,
|
||||
Product product,
|
||||
)? productPopupBuilder,
|
||||
Widget Function(
|
||||
BuildContext context,
|
||||
|
@ -54,14 +45,13 @@ class ProductPageConfiguration {
|
|||
}) {
|
||||
_productPopupBuilder = productPopupBuilder;
|
||||
_productPopupBuilder ??=
|
||||
(BuildContext context, ProductPageProduct product) => ProductItemPopup(
|
||||
(BuildContext context, Product product) => ProductItemPopup(
|
||||
product: product,
|
||||
configuration: this,
|
||||
);
|
||||
|
||||
_onProductDetail = onProductDetail;
|
||||
_onProductDetail ??=
|
||||
(BuildContext context, ProductPageProduct product) async {
|
||||
_onProductDetail ??= (BuildContext context, Product product) async {
|
||||
var theme = Theme.of(context);
|
||||
|
||||
await showModalBottomSheet(
|
||||
|
@ -98,8 +88,8 @@ class ProductPageConfiguration {
|
|||
};
|
||||
|
||||
_getDiscountDescription = getDiscountDescription;
|
||||
_getDiscountDescription ??=
|
||||
(ProductPageProduct product) => "${product.name} is on sale!";
|
||||
_getDiscountDescription ??= (Product product) =>
|
||||
"${product.name}, now for ${product.discountPrice} each";
|
||||
}
|
||||
|
||||
/// The shop that is initially selected.
|
||||
|
@ -119,27 +109,25 @@ class ProductPageConfiguration {
|
|||
/// for each product in their seperated category. This builder should only
|
||||
/// build the widget for one specific product. This builder has a default
|
||||
/// in-case the developer does not override it.
|
||||
Widget Function(BuildContext context, ProductPageProduct product)?
|
||||
productBuilder;
|
||||
Widget Function(BuildContext context, Product product)? productBuilder;
|
||||
|
||||
late Widget Function(BuildContext context, ProductPageProduct product)?
|
||||
late Widget Function(BuildContext context, Product product)?
|
||||
_productPopupBuilder;
|
||||
|
||||
/// The builder for the product popup. This popup will be displayed when the
|
||||
/// user clicks on a product. This builder should only build the widget that
|
||||
/// displays the content of one specific product.
|
||||
/// This builder has a default in-case the developer
|
||||
Widget Function(BuildContext context, ProductPageProduct product)
|
||||
Widget Function(BuildContext context, Product product)
|
||||
get productPopupBuilder => _productPopupBuilder!;
|
||||
|
||||
late Function(BuildContext context, ProductPageProduct product)?
|
||||
_onProductDetail;
|
||||
late Function(BuildContext context, Product 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!;
|
||||
Function(BuildContext context, Product product) get onProductDetail =>
|
||||
_onProductDetail!;
|
||||
|
||||
late Widget Function(BuildContext context)? _noContentBuilder;
|
||||
|
||||
|
@ -149,7 +137,11 @@ class ProductPageConfiguration {
|
|||
|
||||
/// The builder for the shopping cart. This builder should return a widget
|
||||
/// that navigates to the shopping cart overview page.
|
||||
Widget Function(BuildContext context)? navigateToShoppingCartBuilder;
|
||||
Widget Function(
|
||||
BuildContext context,
|
||||
ProductPageConfiguration configuration,
|
||||
ShoppingCartNotifier notifier,
|
||||
) navigateToShoppingCartBuilder;
|
||||
|
||||
late Widget Function(
|
||||
BuildContext context,
|
||||
|
@ -162,16 +154,16 @@ class ProductPageConfiguration {
|
|||
Widget Function(BuildContext context, Object? error, StackTrace? stackTrace)?
|
||||
get errorBuilder => _errorBuilder;
|
||||
|
||||
late String Function(ProductPageProduct product)? _getDiscountDescription;
|
||||
late String Function(Product 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 =>
|
||||
String Function(Product product)? get getDiscountDescription =>
|
||||
_getDiscountDescription!;
|
||||
|
||||
/// This function must be implemented by the developer and should handle the
|
||||
/// adding of a product to the cart.
|
||||
Function(ProductPageProduct product) onAddToCart;
|
||||
Function(Product product) onAddToCart;
|
||||
|
||||
/// This function gets executed when the user changes the shop selection.
|
||||
/// This function always fires upon first load with the initial shop as well.
|
||||
|
@ -198,5 +190,60 @@ class ProductPageConfiguration {
|
|||
final Widget? bottomNavigationBar;
|
||||
|
||||
/// Optional app bar that you can pass to the order detail screen.
|
||||
final PreferredSizeWidget? appBar;
|
||||
final AppBar Function(BuildContext context)? appBar;
|
||||
}
|
||||
|
||||
AppBar _defaultAppBar(
|
||||
BuildContext context,
|
||||
) {
|
||||
var theme = Theme.of(context);
|
||||
|
||||
return AppBar(
|
||||
leading: IconButton(onPressed: () {}, icon: const Icon(Icons.person)),
|
||||
actions: [
|
||||
IconButton(onPressed: () {}, icon: const Icon(Icons.filter_alt)),
|
||||
],
|
||||
title: Text(
|
||||
"Product page",
|
||||
style: theme.textTheme.headlineLarge,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _defaultNavigateToShoppingCartBuilder(
|
||||
BuildContext context,
|
||||
ProductPageConfiguration configuration,
|
||||
ShoppingCartNotifier notifier,
|
||||
) {
|
||||
var theme = Theme.of(context);
|
||||
|
||||
return ListenableBuilder(
|
||||
listenable: notifier,
|
||||
builder: (context, widget) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 60),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton(
|
||||
onPressed: configuration.getProductsInShoppingCart?.call() != 0
|
||||
? 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(
|
||||
configuration.localizations.navigateToShoppingCart,
|
||||
style: theme.textTheme.displayLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import "package:flutter_product_page/flutter_product_page.dart";
|
||||
import "package:flutter_shopping/flutter_shopping.dart";
|
||||
|
||||
/// Return type that contains the products and an optional discounted product.
|
||||
class ProductPageContent {
|
||||
|
@ -9,8 +9,8 @@ class ProductPageContent {
|
|||
});
|
||||
|
||||
/// List of products that belong to the shop.
|
||||
final List<ProductPageProduct> products;
|
||||
final List<Product> products;
|
||||
|
||||
/// Optional highlighted discounted product to display.
|
||||
final ProductPageProduct? discountedProduct;
|
||||
final Product? discountedProduct;
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
class ProductPageLocalization {
|
||||
/// Default constructor
|
||||
const ProductPageLocalization({
|
||||
this.navigateToShoppingCart = "To shopping cart",
|
||||
this.discountTitle = "Discount",
|
||||
this.navigateToShoppingCart = "View shopping cart",
|
||||
this.discountTitle = "Weekly offer",
|
||||
this.failedToLoadImageExplenation = "Failed to load image",
|
||||
this.close = "Close",
|
||||
});
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
/// 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;
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_nested_categories/flutter_nested_categories.dart";
|
||||
import "package:flutter_product_page/flutter_product_page.dart";
|
||||
import "package:flutter_product_page/src/services/shopping_cart_notifier.dart";
|
||||
import "package:flutter_product_page/src/ui/components/product_item.dart";
|
||||
import "package:flutter_shopping/flutter_shopping.dart";
|
||||
|
||||
/// A function that is called when a product is added to the cart.
|
||||
ProductPageProduct onAddToCartWrapper(
|
||||
Product onAddToCartWrapper(
|
||||
ProductPageConfiguration configuration,
|
||||
ShoppingCartNotifier shoppingCartNotifier,
|
||||
ProductPageProduct product,
|
||||
Product product,
|
||||
) {
|
||||
shoppingCartNotifier.productsChanged();
|
||||
|
||||
|
@ -19,13 +19,14 @@ ProductPageProduct onAddToCartWrapper(
|
|||
|
||||
/// Generates a [CategoryList] from a list of [Product]s and a
|
||||
/// [ProductPageConfiguration].
|
||||
CategoryList getCategoryList(
|
||||
Widget getCategoryList(
|
||||
BuildContext context,
|
||||
ProductPageConfiguration configuration,
|
||||
ShoppingCartNotifier shoppingCartNotifier,
|
||||
List<ProductPageProduct> products,
|
||||
List<Product> products,
|
||||
) {
|
||||
var categorizedProducts = <String, List<ProductPageProduct>>{};
|
||||
var theme = Theme.of(context);
|
||||
var categorizedProducts = <String, List<Product>>{};
|
||||
for (var product in products) {
|
||||
if (!categorizedProducts.containsKey(product.category)) {
|
||||
categorizedProducts[product.category] = [];
|
||||
|
@ -43,8 +44,7 @@ CategoryList getCategoryList(
|
|||
: ProductItem(
|
||||
product: product,
|
||||
onProductDetail: configuration.onProductDetail,
|
||||
onAddToCart: (ProductPageProduct product) =>
|
||||
onAddToCartWrapper(
|
||||
onAddToCart: (Product product) => onAddToCartWrapper(
|
||||
configuration,
|
||||
shoppingCartNotifier,
|
||||
product,
|
||||
|
@ -59,15 +59,19 @@ CategoryList getCategoryList(
|
|||
);
|
||||
categories.add(category);
|
||||
});
|
||||
|
||||
return CategoryList(
|
||||
title: configuration.categoryStylingConfiguration.title,
|
||||
titleStyle: configuration.categoryStylingConfiguration.titleStyle,
|
||||
customTitle: configuration.categoryStylingConfiguration.customTitle,
|
||||
headerCentered: configuration.categoryStylingConfiguration.headerCentered,
|
||||
headerStyling: configuration.categoryStylingConfiguration.headerStyling,
|
||||
isCategoryCollapsible:
|
||||
configuration.categoryStylingConfiguration.isCategoryCollapsible,
|
||||
content: categories,
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (var category in categories) ...[
|
||||
Text(
|
||||
category.name!,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
Column(
|
||||
children: category.content,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import "package:cached_network_image/cached_network_image.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_product_page/flutter_product_page.dart";
|
||||
import "package:flutter_shopping/flutter_shopping.dart";
|
||||
import "package:skeletonizer/skeletonizer.dart";
|
||||
|
||||
/// Product item widget.
|
||||
|
@ -15,14 +15,13 @@ class ProductItem extends StatelessWidget {
|
|||
});
|
||||
|
||||
/// Product to display.
|
||||
final ProductPageProduct product;
|
||||
final Product product;
|
||||
|
||||
/// Function to call when the product detail is requested.
|
||||
final Function(BuildContext context, ProductPageProduct selectedProduct)
|
||||
onProductDetail;
|
||||
final Function(BuildContext context, Product selectedProduct) onProductDetail;
|
||||
|
||||
/// Function to call when the product is added to the cart.
|
||||
final Function(ProductPageProduct selectedProduct) onAddToCart;
|
||||
final Function(Product selectedProduct) onAddToCart;
|
||||
|
||||
/// Localizations for the product page.
|
||||
final ProductPageLocalization localizations;
|
||||
|
@ -76,7 +75,10 @@ class ProductItem extends StatelessWidget {
|
|||
padding: const EdgeInsets.only(left: 4),
|
||||
child: IconButton(
|
||||
onPressed: () => onProductDetail(context, product),
|
||||
icon: const Icon(Icons.info_outline),
|
||||
icon: Icon(
|
||||
Icons.info_outline,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -84,10 +86,7 @@ class ProductItem extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
_PriceLabel(
|
||||
price: product.price,
|
||||
discountPrice: (product.hasDiscount && product.discountPrice != null)
|
||||
? product.discountPrice
|
||||
: null,
|
||||
product: product,
|
||||
),
|
||||
_AddToCardButton(
|
||||
product: product,
|
||||
|
@ -113,42 +112,36 @@ class ProductItem extends StatelessWidget {
|
|||
|
||||
class _PriceLabel extends StatelessWidget {
|
||||
const _PriceLabel({
|
||||
required this.price,
|
||||
required this.discountPrice,
|
||||
required this.product,
|
||||
});
|
||||
|
||||
final double price;
|
||||
final double? discountPrice;
|
||||
final Product product;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
|
||||
if (discountPrice == null)
|
||||
return Text(
|
||||
price.toStringAsFixed(2),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
);
|
||||
else
|
||||
return Row(
|
||||
children: [
|
||||
return Row(
|
||||
children: [
|
||||
if (product.hasDiscount) ...[
|
||||
Text(
|
||||
price.toStringAsFixed(2),
|
||||
product.price.toStringAsFixed(2),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 10,
|
||||
color: theme.colorScheme.primary,
|
||||
decoration: TextDecoration.lineThrough,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4.0),
|
||||
child: Text(
|
||||
discountPrice!.toStringAsFixed(2),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
);
|
||||
Text(
|
||||
product.hasDiscount
|
||||
? product.discountPrice!.toStringAsFixed(2)
|
||||
: product.price.toStringAsFixed(2),
|
||||
style: theme.textTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -158,36 +151,30 @@ class _AddToCardButton extends StatelessWidget {
|
|||
required this.onAddToCart,
|
||||
});
|
||||
|
||||
final ProductPageProduct product;
|
||||
final Function(ProductPageProduct product) onAddToCart;
|
||||
final Product product;
|
||||
final Function(Product product) onAddToCart;
|
||||
|
||||
static const double boxSize = 29;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
return SizedBox(
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
width: boxSize,
|
||||
height: boxSize,
|
||||
child: Center(
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: Icon(
|
||||
icon: const Icon(
|
||||
Icons.add,
|
||||
color: theme.primaryColor,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () => onAddToCart(product),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
theme.colorScheme.secondary,
|
||||
),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import "package:cached_network_image/cached_network_image.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_product_page/flutter_product_page.dart";
|
||||
import "package:flutter_shopping/flutter_shopping.dart";
|
||||
|
||||
/// A widget that displays a weekly discount.
|
||||
class WeeklyDiscount extends StatelessWidget {
|
||||
|
@ -15,10 +15,10 @@ class WeeklyDiscount extends StatelessWidget {
|
|||
final ProductPageConfiguration configuration;
|
||||
|
||||
/// The product for which the discount is displayed.
|
||||
final ProductPageProduct product;
|
||||
final Product product;
|
||||
|
||||
/// The top padding of the widget.
|
||||
static const double topPadding = 32.0;
|
||||
static const double topPadding = 20;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -28,9 +28,7 @@ class WeeklyDiscount extends StatelessWidget {
|
|||
padding: const EdgeInsets.all(20.0),
|
||||
child: Text(
|
||||
configuration.getDiscountDescription!(product),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
);
|
||||
|
@ -73,9 +71,9 @@ class WeeklyDiscount extends StatelessWidget {
|
|||
);
|
||||
|
||||
var topText = DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.primaryColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.black,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(4),
|
||||
topRight: Radius.circular(4),
|
||||
),
|
||||
|
@ -88,10 +86,8 @@ class WeeklyDiscount extends StatelessWidget {
|
|||
horizontal: 16,
|
||||
),
|
||||
child: Text(
|
||||
configuration.localizations.discountTitle.toUpperCase(),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
configuration.localizations.discountTitle,
|
||||
style: theme.textTheme.headlineSmall,
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
|
@ -100,7 +96,6 @@ class WeeklyDiscount extends StatelessWidget {
|
|||
|
||||
var boxDecoration = BoxDecoration(
|
||||
border: Border.all(
|
||||
color: theme.primaryColor,
|
||||
width: 1.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
|
|
|
@ -121,6 +121,7 @@ class _ProductPage extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
var pageContent = SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ShopSelector(
|
||||
configuration: configuration,
|
||||
|
@ -142,63 +143,13 @@ class _ProductPage extends StatelessWidget {
|
|||
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,
|
||||
),
|
||||
child: configuration.navigateToShoppingCartBuilder(
|
||||
context,
|
||||
configuration,
|
||||
shoppingCartNotifier,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -215,74 +166,87 @@ class _ShopContents extends StatelessWidget {
|
|||
final ShoppingCartNotifier shoppingCartNotifier;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: configuration.pagePadding.horizontal,
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: configuration.pagePadding.horizontal,
|
||||
),
|
||||
child: FutureBuilder(
|
||||
// ignore: discarded_futures
|
||||
future: configuration.getProducts(
|
||||
selectedShopService.selectedShop!,
|
||||
),
|
||||
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),
|
||||
],
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Align(
|
||||
alignment: Alignment.center,
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
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, 12, 16, 8),
|
||||
child: Column(
|
||||
children: [
|
||||
// Discounted product
|
||||
if (productPageContent.discountedProduct != null) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: WeeklyDiscount(
|
||||
configuration: configuration,
|
||||
product: productPageContent.discountedProduct!,
|
||||
),
|
||||
),
|
||||
],
|
||||
// Products
|
||||
getCategoryList(
|
||||
context,
|
||||
configuration,
|
||||
shoppingCartNotifier,
|
||||
productPageContent.products,
|
||||
),
|
||||
|
||||
productList,
|
||||
// Bottom padding so the last product is not cut off
|
||||
// by the to shopping cart button.
|
||||
const SizedBox(height: 48),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Discounted product
|
||||
if (productPageContent.discountedProduct != null) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: WeeklyDiscount(
|
||||
configuration: configuration,
|
||||
product: productPageContent.discountedProduct!,
|
||||
),
|
||||
),
|
||||
],
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
||||
child: Text(
|
||||
"What would you like to order?",
|
||||
style: theme.textTheme.titleLarge,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
),
|
||||
|
||||
productList,
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,13 +24,13 @@ class ProductPageScreen extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: configuration.appBar!.call(context),
|
||||
body: SafeArea(
|
||||
child: ProductPage(
|
||||
configuration: configuration,
|
||||
initialBuildShopId: initialBuildShopId,
|
||||
),
|
||||
),
|
||||
appBar: configuration.appBar,
|
||||
bottomNavigationBar: configuration.bottomNavigationBar,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ class HorizontalListItems extends StatelessWidget {
|
|||
required this.selectedItem,
|
||||
required this.onTap,
|
||||
this.paddingBetweenButtons = 2.0,
|
||||
this.paddingOnButtons = 4,
|
||||
this.paddingOnButtons = 6,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
@ -32,41 +32,46 @@ class HorizontalListItems extends StatelessWidget {
|
|||
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,
|
||||
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,
|
||||
),
|
||||
),
|
||||
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(),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_product_page/flutter_product_page.dart";
|
||||
import "package:flutter_shopping/flutter_shopping.dart";
|
||||
|
||||
/// A popup that displays the product item.
|
||||
class ProductItemPopup extends StatelessWidget {
|
||||
|
@ -11,7 +11,7 @@ class ProductItemPopup extends StatelessWidget {
|
|||
});
|
||||
|
||||
/// The product to display.
|
||||
final ProductPageProduct product;
|
||||
final Product product;
|
||||
|
||||
/// Configuration for the product page.
|
||||
final ProductPageConfiguration configuration;
|
||||
|
@ -20,48 +20,44 @@ class ProductItemPopup extends StatelessWidget {
|
|||
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,
|
||||
],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
product.description,
|
||||
style: theme.textTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20, left: 40, right: 40),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
style: theme.filledButtonTheme.style?.copyWith(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: Text(
|
||||
configuration.localizations.close,
|
||||
style: theme.textTheme.displayLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -33,118 +33,44 @@ class SpacedWrap extends StatelessWidget {
|
|||
/// Callback when an item is tapped.
|
||||
final Function(ProductPageShop shop) onTap;
|
||||
|
||||
Row _buildRow(
|
||||
BuildContext context,
|
||||
List<int> currentRow,
|
||||
double availableRowLength,
|
||||
) {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
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,7 +1,7 @@
|
|||
name: flutter_product_page
|
||||
description: "A Flutter module for the product page"
|
||||
publish_to: 'none'
|
||||
version: 1.0.0
|
||||
version: 2.0.0
|
||||
|
||||
environment:
|
||||
sdk: '>=3.3.4 <4.0.0'
|
||||
|
@ -15,6 +15,11 @@ dependencies:
|
|||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_nested_categories
|
||||
ref: 0.0.1
|
||||
flutter_shopping:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_shopping
|
||||
path: packages/flutter_shopping
|
||||
ref: 2.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -26,13 +31,3 @@ dev_dependencies:
|
|||
|
||||
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
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
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";
|
||||
|
@ -7,7 +6,7 @@ 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([]);
|
||||
final ProductService<Product> productService = ProductService([]);
|
||||
|
||||
FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
|
||||
FlutterShoppingConfiguration(
|
||||
|
@ -24,8 +23,7 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
|
|||
shops: Future.value(getShops()),
|
||||
|
||||
// (REQUIRED): Function to add a product to the cart
|
||||
onAddToCart: (ProductPageProduct product) =>
|
||||
productService.addProduct(product as MyProduct),
|
||||
onAddToCart: productService.addProduct,
|
||||
|
||||
// (REQUIRED): Function to get the products for a shop
|
||||
getProducts: (ProductPageShop shop) =>
|
||||
|
@ -34,7 +32,7 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
|
|||
),
|
||||
|
||||
// (REQUIRED): Function to navigate to the shopping cart
|
||||
onNavigateToShoppingCart: () => onCompleteProductPage(context),
|
||||
onNavigateToShoppingCart: () async => onCompleteProductPage(context),
|
||||
|
||||
// (RECOMMENDED): Function to get the number of products in the
|
||||
// shopping cart. This is used to display the number of products
|
||||
|
@ -43,7 +41,7 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
|
|||
|
||||
// (RECOMMENDED) Function that returns the description for a
|
||||
// product that is on sale.
|
||||
getDiscountDescription: (ProductPageProduct product) =>
|
||||
getDiscountDescription: (product) =>
|
||||
"""${product.name} for just \$${product.discountPrice?.toStringAsFixed(2)}""",
|
||||
|
||||
// (RECOMMENDED) Function that is fired when the shop selection
|
||||
|
@ -60,7 +58,7 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
|
|||
localizations: const ProductPageLocalization(),
|
||||
|
||||
// (OPTIONAL) Appbar
|
||||
appBar: AppBar(
|
||||
appBar: (context) => AppBar(
|
||||
title: const Text("Shop"),
|
||||
leading: IconButton(
|
||||
icon: const Icon(
|
||||
|
@ -85,7 +83,8 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
|
|||
productService: productService,
|
||||
|
||||
// (REQUIRED) product item builder:
|
||||
productItemBuilder: (context, locale, product) => ListTile(
|
||||
productItemBuilder: (context, locale, product, service, config) =>
|
||||
ListTile(
|
||||
title: Text(product.name),
|
||||
subtitle: Text(product.price.toStringAsFixed(2)),
|
||||
leading: Image.network(
|
||||
|
@ -116,14 +115,11 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
|
|||
|
||||
// (OPTIONAL/REQUIRED) on confirm order callback:
|
||||
// Either use this callback or the placeOrderButtonBuilder.
|
||||
onConfirmOrder: (products) => onCompleteShoppingCart(context),
|
||||
onConfirmOrder: (products) async => 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(
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
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) {
|
||||
void storeOrderInDatabase(List<Product> products, OrderResult result) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import "package:example/src/models/my_product.dart";
|
||||
import "package:example/src/models/my_shop.dart";
|
||||
import "package:flutter_shopping/flutter_shopping.dart";
|
||||
|
||||
|
@ -20,8 +19,8 @@ ProductPageContent getShopContent(String shopId) {
|
|||
|
||||
/// 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(
|
||||
List<Product> getProducts(String shopId) => <Product>[
|
||||
Product(
|
||||
id: "1",
|
||||
name: "White bread",
|
||||
price: 2.99,
|
||||
|
@ -29,19 +28,22 @@ List<MyProduct> getProducts(String shopId) => <MyProduct>[
|
|||
imageUrl: "https://via.placeholder.com/150",
|
||||
hasDiscount: true,
|
||||
discountPrice: 1.99,
|
||||
description: "",
|
||||
),
|
||||
MyProduct(
|
||||
Product(
|
||||
id: "2",
|
||||
name: "Brown bread",
|
||||
price: 2.99,
|
||||
category: "Loaves",
|
||||
imageUrl: "https://via.placeholder.com/150",
|
||||
description: "",
|
||||
),
|
||||
MyProduct(
|
||||
Product(
|
||||
id: "3",
|
||||
name: "Cheese sandwich",
|
||||
price: 1.99,
|
||||
category: "Sandwiches",
|
||||
imageUrl: "https://via.placeholder.com/150",
|
||||
description: "",
|
||||
),
|
||||
];
|
||||
|
|
|
@ -12,12 +12,11 @@ dependencies:
|
|||
flutter_hooks: ^0.20.0
|
||||
hooks_riverpod: ^2.1.1
|
||||
go_router: 12.1.3
|
||||
|
||||
# Iconica packages
|
||||
|
||||
## Userstories
|
||||
flutter_shopping:
|
||||
path: ../
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_shopping
|
||||
path: packages/flutter_shopping
|
||||
ref: 2.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
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";
|
||||
|
@ -6,7 +5,7 @@ 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([]);
|
||||
final ProductService<Product> productService = ProductService([]);
|
||||
|
||||
FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
|
||||
FlutterShoppingConfiguration(
|
||||
|
@ -27,8 +26,9 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
|
|||
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),
|
||||
onAddToCart: (product) {
|
||||
return productService.addProduct(product);
|
||||
},
|
||||
|
||||
// (REQUIRED): Function to get the products for a shop
|
||||
getProducts: (ProductPageShop shop) =>
|
||||
|
@ -41,7 +41,9 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
|
|||
|
||||
shopSelectorStyle: ShopSelectorStyle.row,
|
||||
|
||||
navigateToShoppingCartBuilder: (context) => const SizedBox.shrink(),
|
||||
navigateToShoppingCartBuilder: (context, productpageinfo, shop) {
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
fixedColor: theme.primaryColor,
|
||||
|
@ -164,7 +166,7 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
|
|||
const SizedBox(height: 12),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
productService.addProduct(product as MyProduct);
|
||||
productService.addProduct(product);
|
||||
},
|
||||
child: const Text("In winkelwagen"),
|
||||
),
|
||||
|
@ -206,7 +208,7 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
|
|||
),
|
||||
|
||||
// (OPTIONAL) Appbar
|
||||
appBar: AppBar(
|
||||
appBar: (context) => AppBar(
|
||||
title: const SizedBox(
|
||||
height: 40,
|
||||
child: SearchBar(
|
||||
|
@ -262,7 +264,8 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
|
|||
productService: productService,
|
||||
|
||||
// (REQUIRED) product item builder:
|
||||
productItemBuilder: (context, locale, product) => ListTile(
|
||||
productItemBuilder: (context, locale, product, service, config) =>
|
||||
ListTile(
|
||||
title: Text(product.name),
|
||||
subtitle: Text(product.price.toStringAsFixed(2)),
|
||||
leading: Image.network(
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
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 = {
|
||||
|
@ -8,8 +7,8 @@ Map<String, String> categories = {
|
|||
"TV's": "TV's",
|
||||
};
|
||||
|
||||
List<MyProduct> allProducts() => [
|
||||
MyProduct(
|
||||
List<Product> allProducts() => [
|
||||
Product(
|
||||
id: "1",
|
||||
name:
|
||||
"Skar Audio Single 8\" Complete 1,200 Watt EVL Series Subwoofer Bass Package - Includes Loaded Enclosure with...",
|
||||
|
@ -17,8 +16,9 @@ List<MyProduct> allProducts() => [
|
|||
category: categories["Electronics"]!,
|
||||
imageUrl:
|
||||
"https://m.media-amazon.com/images/I/710n3hnbfXL._AC_UY218_.jpg",
|
||||
description: "",
|
||||
),
|
||||
MyProduct(
|
||||
Product(
|
||||
id: "2",
|
||||
name:
|
||||
"Frameo 10.1 Inch WiFi Digital Picture Frame, 1280x800 HD IPS Touch Screen Photo Frame Electronic, 32GB Memory, Auto...",
|
||||
|
@ -26,8 +26,9 @@ List<MyProduct> allProducts() => [
|
|||
category: categories["Electronics"]!,
|
||||
imageUrl:
|
||||
"https://m.media-amazon.com/images/I/61O+aorCp0L._AC_UY218_.jpg",
|
||||
description: "",
|
||||
),
|
||||
MyProduct(
|
||||
Product(
|
||||
id: "3",
|
||||
name:
|
||||
"STREBITO Electronics Precision Screwdriver Sets 142-Piece with 120 Bits Magnetic Repair Tool Kit for iPhone, MacBook,...",
|
||||
|
@ -35,8 +36,9 @@ List<MyProduct> allProducts() => [
|
|||
category: categories["Electronics"]!,
|
||||
imageUrl:
|
||||
"https://m.media-amazon.com/images/I/81-C7lGtQsL._AC_UY218_.jpg",
|
||||
description: "",
|
||||
),
|
||||
MyProduct(
|
||||
Product(
|
||||
id: "4",
|
||||
name:
|
||||
"Samsung Galaxy A15 (SM-155M/DSN), 128GB 6GB RAM, Dual SIM, Factory Unlocked GSM, International Version (Wall...",
|
||||
|
@ -44,8 +46,9 @@ List<MyProduct> allProducts() => [
|
|||
category: categories["Smart phones"]!,
|
||||
imageUrl:
|
||||
"https://m.media-amazon.com/images/I/51rp0nqaPoL._AC_UY218_.jpg",
|
||||
description: "",
|
||||
),
|
||||
MyProduct(
|
||||
Product(
|
||||
id: "5",
|
||||
name:
|
||||
"SAMSUNG Galaxy S24 Ultra Cell Phone, 512GB AI Smartphone, Unlocked Android, 50MP Zoom Camera, Long...",
|
||||
|
@ -53,6 +56,7 @@ List<MyProduct> allProducts() => [
|
|||
category: categories["Smart phones"]!,
|
||||
imageUrl:
|
||||
"https://m.media-amazon.com/images/I/71ZoDT7a2wL._AC_UY218_.jpg",
|
||||
description: "",
|
||||
),
|
||||
];
|
||||
|
||||
|
@ -72,7 +76,7 @@ ProductPageContent getShopContent(String shopId) {
|
|||
);
|
||||
}
|
||||
|
||||
List<MyProduct> getProducts(String categoryId) {
|
||||
List<Product> getProducts(String categoryId) {
|
||||
if (categoryId == "1") {
|
||||
return allProducts();
|
||||
} else if (categoryId == "2") {
|
||||
|
|
|
@ -17,7 +17,10 @@ dependencies:
|
|||
url: https://github.com/Iconica-Development/flutter_nested_categories
|
||||
ref: 0.0.1
|
||||
flutter_shopping:
|
||||
path: ../
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_shopping
|
||||
path: packages/flutter_shopping
|
||||
ref: 2.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
@ -6,6 +6,7 @@ export "package:flutter_product_page/flutter_product_page.dart";
|
|||
export "package:flutter_shopping_cart/flutter_shopping_cart.dart";
|
||||
|
||||
export "src/config/flutter_shopping_configuration.dart";
|
||||
export "src/models/product.dart";
|
||||
export "src/routes.dart";
|
||||
export "src/user_stores/flutter_shopping_userstory_go_router.dart";
|
||||
export "src/user_stores/flutter_shopping_userstory_navigation.dart";
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
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,10 @@
|
|||
/// Contains the localized strings for the order details screen.
|
||||
class OrderDetailsLocalizations {
|
||||
/// Creates order details localizations
|
||||
OrderDetailsLocalizations({
|
||||
this.orderDetailsTitle = "Information",
|
||||
});
|
||||
|
||||
/// Title for the order details screen.
|
||||
final String orderDetailsTitle;
|
||||
}
|
43
packages/flutter_shopping/lib/src/models/product.dart
Normal file
43
packages/flutter_shopping/lib/src/models/product.dart
Normal file
|
@ -0,0 +1,43 @@
|
|||
/// The product class contains all the information that a product can have.
|
||||
/// This class is used in the shopping cart and the product page.
|
||||
class Product {
|
||||
/// Creates a product.
|
||||
Product({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.imageUrl,
|
||||
required this.category,
|
||||
required this.price,
|
||||
required this.description,
|
||||
this.hasDiscount = false,
|
||||
this.discountPrice,
|
||||
this.quantity = 1,
|
||||
});
|
||||
|
||||
/// The unique identifier for the product.
|
||||
final String id;
|
||||
|
||||
/// The name of the product.
|
||||
final String name;
|
||||
|
||||
/// The image URL of the product.
|
||||
final String imageUrl;
|
||||
|
||||
/// The category of the product.
|
||||
final String category;
|
||||
|
||||
/// The price of the product.
|
||||
final double price;
|
||||
|
||||
/// Whether the product has a discount or not.
|
||||
final bool hasDiscount;
|
||||
|
||||
/// The discounted price of the product. Only used if [hasDiscount] is true.
|
||||
final double? discountPrice;
|
||||
|
||||
/// Quantity for the product.
|
||||
int quantity;
|
||||
|
||||
/// The description of the product.
|
||||
final String description;
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
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";
|
||||
|
@ -29,8 +28,11 @@ List<GoRoute> getShoppingStoryRoutes({
|
|||
builder: (context, state) => configuration.orderDetailsBuilder != null
|
||||
? configuration.orderDetailsBuilder!(context)
|
||||
: OrderDetailScreen(
|
||||
configuration:
|
||||
getDefaultOrderDetailConfiguration(context, configuration),
|
||||
configuration: OrderDetailConfiguration(
|
||||
onCompleted: (result) {
|
||||
context.go(FlutterShoppingPathRoutes.orderSuccess);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
|
|
|
@ -35,18 +35,18 @@ Future<void> onCompleteOrderDetails(
|
|||
///
|
||||
/// You can create your own implementation if you decide to use a different
|
||||
/// approach.
|
||||
void onCompleteShoppingCart(
|
||||
Future<void> onCompleteShoppingCart(
|
||||
BuildContext context,
|
||||
) {
|
||||
context.go(FlutterShoppingPathRoutes.orderDetails);
|
||||
) async {
|
||||
await context.push(FlutterShoppingPathRoutes.orderDetails);
|
||||
}
|
||||
|
||||
/// Default on complete product page function.
|
||||
///
|
||||
/// You can create your own implementation if you decide to use a different
|
||||
/// approach.
|
||||
void onCompleteProductPage(
|
||||
Future<void> onCompleteProductPage(
|
||||
BuildContext context,
|
||||
) {
|
||||
context.go(FlutterShoppingPathRoutes.shoppingCart);
|
||||
) async {
|
||||
await context.push(FlutterShoppingPathRoutes.shoppingCart);
|
||||
}
|
||||
|
|
|
@ -16,51 +16,186 @@ class DefaultOrderSucces extends StatelessWidget {
|
|||
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(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
"Confirmation",
|
||||
style: theme.textTheme.headlineLarge,
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: content,
|
||||
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 Peter for your order!",
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Text(
|
||||
"The order was placed at Bakkerij de Goudkorst."
|
||||
" You can pick this"
|
||||
" up on Monday, February 7 at 1:00 PM.",
|
||||
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),
|
||||
const SizedBox(width: 32),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 60),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton(
|
||||
onPressed: () async {
|
||||
configuration.onCompleteUserStory.call(context);
|
||||
},
|
||||
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) {
|
||||
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: const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10),
|
||||
),
|
||||
child: Image.network(
|
||||
"https://picsum.photos/150",
|
||||
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(
|
||||
"Butcher Puurvlees",
|
||||
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(
|
||||
"Chicken legs, now for 4,99",
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
name: flutter_shopping
|
||||
description: "A new Flutter project."
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
version: 1.0.0
|
||||
publish_to: 'none'
|
||||
version: 2.0.0
|
||||
|
||||
environment:
|
||||
sdk: '>=3.3.4 <4.0.0'
|
||||
|
@ -13,17 +13,17 @@ dependencies:
|
|||
flutter_product_page:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_shopping
|
||||
ref: 1.0.0
|
||||
ref: 2.0.0
|
||||
path: packages/flutter_product_page
|
||||
flutter_shopping_cart:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_shopping
|
||||
ref: 1.0.0
|
||||
ref: 2.0.0
|
||||
path: packages/flutter_shopping_cart
|
||||
flutter_order_details:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_shopping
|
||||
ref: 1.0.0
|
||||
ref: 2.0.0
|
||||
path: packages/flutter_order_details
|
||||
|
||||
dev_dependencies:
|
||||
|
@ -35,13 +35,4 @@ dev_dependencies:
|
|||
ref: 7.0.0
|
||||
|
||||
flutter:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
|
|
|
@ -3,6 +3,5 @@ library flutter_shopping_cart;
|
|||
|
||||
export "src/config/shopping_cart_config.dart";
|
||||
export "src/config/shopping_cart_localizations.dart";
|
||||
export "src/models/shopping_cart_product.dart";
|
||||
export "src/services/product_service.dart";
|
||||
export "src/widgets/shopping_cart_screen.dart";
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
|
||||
import "package:flutter_shopping/flutter_shopping.dart";
|
||||
import "package:flutter_shopping_cart/src/widgets/product_item_popup.dart";
|
||||
|
||||
Widget _defaultNoContentBuilder(BuildContext context) =>
|
||||
const SizedBox.shrink();
|
||||
|
@ -7,30 +8,21 @@ Widget _defaultNoContentBuilder(BuildContext context) =>
|
|||
/// Shopping cart configuration
|
||||
///
|
||||
/// This class is used to configure the shopping cart.
|
||||
class ShoppingCartConfig<T extends ShoppingCartProduct> {
|
||||
class ShoppingCartConfig<T extends Product> {
|
||||
/// Creates a shopping cart configuration.
|
||||
ShoppingCartConfig({
|
||||
required this.productService,
|
||||
//
|
||||
this.productItemBuilder = _defaultProductItemBuilder,
|
||||
this.onConfirmOrder,
|
||||
this.confirmOrderButtonBuilder,
|
||||
this.confirmOrderButtonHeight = 100,
|
||||
//
|
||||
this.sumBottomSheetBuilder,
|
||||
this.sumBottomSheetHeight = 100,
|
||||
//
|
||||
this.title,
|
||||
this.titleBuilder,
|
||||
//
|
||||
this.localizations = const ShoppingCartLocalizations(),
|
||||
//
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 32),
|
||||
this.bottomPadding = const EdgeInsets.fromLTRB(44, 0, 44, 32),
|
||||
//
|
||||
this.appBar,
|
||||
//
|
||||
Widget Function(BuildContext context, Locale locale, T product)?
|
||||
productItemBuilder,
|
||||
Widget Function(BuildContext context) noContentBuilder =
|
||||
_defaultNoContentBuilder,
|
||||
}) : assert(
|
||||
|
@ -45,17 +37,7 @@ you cannot use the onConfirmOrder callback.""",
|
|||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
_noContentBuilder = noContentBuilder;
|
||||
|
||||
/// Product Service. The service contains all the products that
|
||||
/// a shopping cart can contain. Each product must extend the [Product] class.
|
||||
|
@ -65,13 +47,15 @@ you must use the onConfirmOrder callback.""",
|
|||
/// 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
|
||||
/// that will be displayed in the shopping cart.
|
||||
Widget Function(BuildContext context, Locale locale, T product)
|
||||
get productItemBuilder => _productItemBuilder!;
|
||||
final Widget Function(
|
||||
BuildContext context,
|
||||
Locale locale,
|
||||
Product product,
|
||||
ProductService<Product> productService,
|
||||
ShoppingCartConfig configuration,
|
||||
) productItemBuilder;
|
||||
|
||||
final Widget Function(BuildContext context) _noContentBuilder;
|
||||
|
||||
|
@ -115,10 +99,6 @@ you must use the onConfirmOrder callback.""",
|
|||
/// [sumBottomSheetBuilder] is overridden.
|
||||
final EdgeInsets bottomPadding;
|
||||
|
||||
/// Title of the shopping cart. The title is displayed at the top of the
|
||||
/// shopping cart. If you provide a title builder, the title will be ignored.
|
||||
final String? title;
|
||||
|
||||
/// Title builder. This builder is used to build the title of the shopping
|
||||
/// cart. The title is displayed at the top of the shopping cart. If you
|
||||
/// use the title builder, the [title] will be ignored.
|
||||
|
@ -131,3 +111,99 @@ you must use the onConfirmOrder callback.""",
|
|||
/// App bar for the shopping cart screen.
|
||||
final PreferredSizeWidget? appBar;
|
||||
}
|
||||
|
||||
Widget _defaultProductItemBuilder(
|
||||
BuildContext context,
|
||||
Locale locale,
|
||||
Product product,
|
||||
ProductService<Product> service,
|
||||
ShoppingCartConfig configuration,
|
||||
) {
|
||||
var theme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.only(top: 3, left: 4, bottom: 3),
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
product.name,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: theme.colorScheme.surface,
|
||||
builder: (context) => ProductItemPopup(
|
||||
product: product,
|
||||
configuration: configuration,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.info_outline,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
leading: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: Image.network(
|
||||
product.imageUrl,
|
||||
),
|
||||
),
|
||||
trailing: Column(
|
||||
children: [
|
||||
Text(
|
||||
product.price.toStringAsFixed(2),
|
||||
style: theme.textTheme.labelSmall,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
constraints: const BoxConstraints(),
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(
|
||||
Icons.remove,
|
||||
color: Colors.black,
|
||||
),
|
||||
onPressed: () => service.removeOneProduct(product),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
height: 30,
|
||||
width: 30,
|
||||
child: Text(
|
||||
"${product.quantity}",
|
||||
style: theme.textTheme.titleSmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
constraints: const BoxConstraints(),
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(
|
||||
Icons.add,
|
||||
color: Colors.black,
|
||||
),
|
||||
onPressed: () => service.addProduct(product),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,8 +5,10 @@ class ShoppingCartLocalizations {
|
|||
/// Creates shopping cart localizations
|
||||
const ShoppingCartLocalizations({
|
||||
this.locale = const Locale("en", "US"),
|
||||
this.placeOrder = "PLACE ORDER",
|
||||
this.sum = "Total:",
|
||||
this.placeOrder = "Order",
|
||||
this.sum = "Subtotal:",
|
||||
this.cartTitle = "Products",
|
||||
this.close = "close",
|
||||
});
|
||||
|
||||
/// Locale for the shopping cart.
|
||||
|
@ -21,4 +23,11 @@ class ShoppingCartLocalizations {
|
|||
|
||||
/// Localization for the sum.
|
||||
final String sum;
|
||||
|
||||
/// Title for the shopping cart. This title will be displayed at the top of
|
||||
/// the shopping cart.
|
||||
final String cartTitle;
|
||||
|
||||
/// Localization for the close button for the popup.
|
||||
final String close;
|
||||
}
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
/// 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;
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
|
||||
import "package:flutter_shopping/flutter_shopping.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 {
|
||||
class ProductService<T extends Product> extends ChangeNotifier {
|
||||
/// Creates a product service.
|
||||
ProductService(this.products);
|
||||
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_shopping/flutter_shopping.dart";
|
||||
|
||||
/// A popup that displays the product item.
|
||||
class ProductItemPopup extends StatelessWidget {
|
||||
/// Constructor for the product item popup.
|
||||
const ProductItemPopup({
|
||||
required this.product,
|
||||
required this.configuration,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The product to display.
|
||||
final Product product;
|
||||
|
||||
/// Configuration for the product page.
|
||||
final ShoppingCartConfig configuration;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
product.description,
|
||||
style: theme.textTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20, left: 40, right: 40),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
style: theme.filledButtonTheme.style?.copyWith(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: Text(
|
||||
configuration.localizations.close,
|
||||
style: theme.textTheme.displayLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
|
||||
import "package:flutter_shopping/flutter_shopping.dart";
|
||||
|
||||
/// Shopping cart screen widget.
|
||||
class ShoppingCartScreen<T extends ShoppingCartProduct>
|
||||
extends StatelessWidget {
|
||||
class ShoppingCartScreen<T extends Product> extends StatelessWidget {
|
||||
/// Creates a shopping cart screen.
|
||||
const ShoppingCartScreen({
|
||||
required this.configuration,
|
||||
|
@ -22,12 +21,19 @@ class ShoppingCartScreen<T extends ShoppingCartProduct>
|
|||
children: [
|
||||
if (configuration.titleBuilder != null) ...{
|
||||
configuration.titleBuilder!(context),
|
||||
} else if (configuration.title != null) ...{
|
||||
} else ...{
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||
child: Text(
|
||||
configuration.title!,
|
||||
style: theme.textTheme.titleLarge,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 32,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
configuration.localizations.cartTitle,
|
||||
style: theme.textTheme.titleLarge,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
},
|
||||
|
@ -47,6 +53,8 @@ class ShoppingCartScreen<T extends ShoppingCartProduct>
|
|||
context,
|
||||
configuration.localizations.locale,
|
||||
product,
|
||||
configuration.productService,
|
||||
configuration,
|
||||
),
|
||||
// Additional whitespace at the bottom to make sure the
|
||||
// last product(s) are not hidden by the bottom sheet.
|
||||
|
@ -62,54 +70,36 @@ class ShoppingCartScreen<T extends ShoppingCartProduct>
|
|||
),
|
||||
);
|
||||
|
||||
var bottomHeight = configuration.confirmOrderButtonHeight +
|
||||
configuration.sumBottomSheetHeight;
|
||||
|
||||
var bottomBlur = Container(
|
||||
height: bottomHeight,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
theme.colorScheme.surface.withOpacity(0),
|
||||
theme.colorScheme.surface.withOpacity(.5),
|
||||
theme.colorScheme.surface.withOpacity(.8),
|
||||
theme.colorScheme.surface.withOpacity(.8),
|
||||
theme.colorScheme.surface.withOpacity(.8),
|
||||
theme.colorScheme.surface.withOpacity(.8),
|
||||
theme.colorScheme.surface.withOpacity(1),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: configuration.appBar,
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Padding(
|
||||
padding: configuration.padding,
|
||||
child: productBuilder,
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: bottomBlur,
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: _BottomSheet<T>(
|
||||
configuration: configuration,
|
||||
appBar: configuration.appBar ??
|
||||
AppBar(
|
||||
title: Text(
|
||||
"Shopping cart",
|
||||
style: theme.textTheme.headlineLarge,
|
||||
),
|
||||
),
|
||||
],
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Padding(
|
||||
padding: configuration.padding,
|
||||
child: productBuilder,
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: _BottomSheet<T>(
|
||||
configuration: configuration,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BottomSheet<T extends ShoppingCartProduct> extends StatelessWidget {
|
||||
class _BottomSheet<T extends Product> extends StatelessWidget {
|
||||
const _BottomSheet({
|
||||
required this.configuration,
|
||||
super.key,
|
||||
|
@ -145,8 +135,7 @@ class _BottomSheet<T extends ShoppingCartProduct> extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _DefaultConfirmOrderButton<T extends ShoppingCartProduct>
|
||||
extends StatelessWidget {
|
||||
class _DefaultConfirmOrderButton<T extends Product> extends StatelessWidget {
|
||||
const _DefaultConfirmOrderButton({
|
||||
required this.configuration,
|
||||
});
|
||||
|
@ -169,26 +158,27 @@ class _DefaultConfirmOrderButton<T extends ShoppingCartProduct>
|
|||
configuration.onConfirmOrder!(products);
|
||||
}
|
||||
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 80),
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 60),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton(
|
||||
onPressed: () => onConfirmOrderPressed(
|
||||
configuration.productService.products,
|
||||
),
|
||||
style: theme.filledButtonTheme.style?.copyWith(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Text(
|
||||
"""${configuration.localizations.placeOrder} (${configuration.productService.countProducts()})""",
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
configuration.localizations.placeOrder,
|
||||
style: theme.textTheme.displayLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -222,8 +212,8 @@ class _DefaultSumBottomSheet extends StatelessWidget {
|
|||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
totalPrice.toStringAsFixed(2),
|
||||
style: theme.textTheme.titleMedium,
|
||||
"€ ${totalPrice.toStringAsFixed(2)}",
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
name: flutter_shopping_cart
|
||||
description: "A Flutter module for a shopping cart."
|
||||
version: 1.0.0
|
||||
version: 2.0.0
|
||||
publish_to: 'none'
|
||||
|
||||
environment:
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
|
@ -9,6 +10,12 @@ environment:
|
|||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_shopping:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_shopping
|
||||
path: packages/flutter_shopping
|
||||
ref: 2.0.0
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
Loading…
Reference in a new issue