mirror of
https://github.com/Iconica-Development/flutter_shopping.git
synced 2025-05-19 08:53:46 +02:00
feat: add filter screen and widgets
This commit is contained in:
parent
b736072497
commit
db1299f22b
8 changed files with 209 additions and 23 deletions
|
@ -0,0 +1,67 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_product_page/flutter_product_page.dart";
|
||||||
|
|
||||||
|
/// Category selection screen.
|
||||||
|
class CategorySelectionScreen extends StatelessWidget {
|
||||||
|
/// Constructor for the category selection screen.
|
||||||
|
const CategorySelectionScreen({
|
||||||
|
required this.configuration,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Configuration for the product page.
|
||||||
|
final ProductPageConfiguration configuration;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: const SizedBox.shrink(),
|
||||||
|
title: Text(
|
||||||
|
"filter",
|
||||||
|
style: theme.textTheme.headlineLarge,
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: ListenableBuilder(
|
||||||
|
listenable: configuration.shoppingService.productService,
|
||||||
|
builder: (context, _) => Column(
|
||||||
|
children: [
|
||||||
|
...configuration.shoppingService.productService.getCategories().map(
|
||||||
|
(category) {
|
||||||
|
var isChecked = configuration
|
||||||
|
.shoppingService.productService.selectedCategories
|
||||||
|
.contains(category);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: CheckboxListTile(
|
||||||
|
activeColor: theme.colorScheme.primary,
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
value: isChecked,
|
||||||
|
onChanged: (value) {
|
||||||
|
configuration.shoppingService.productService
|
||||||
|
.selectCategory(category);
|
||||||
|
},
|
||||||
|
shape: const UnderlineInputBorder(),
|
||||||
|
title: Text(
|
||||||
|
category,
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ class ProductPageConfiguration {
|
||||||
this.shopselectorBuilder,
|
this.shopselectorBuilder,
|
||||||
this.discountBuilder,
|
this.discountBuilder,
|
||||||
this.categoryListBuilder,
|
this.categoryListBuilder,
|
||||||
|
this.selectedCategoryBuilder,
|
||||||
}) {
|
}) {
|
||||||
onProductDetail ??= _onProductDetail;
|
onProductDetail ??= _onProductDetail;
|
||||||
discountDescription ??= _defaultDiscountDescription;
|
discountDescription ??= _defaultDiscountDescription;
|
||||||
|
@ -143,6 +144,10 @@ class ProductPageConfiguration {
|
||||||
ProductPageConfiguration configuration,
|
ProductPageConfiguration configuration,
|
||||||
List<Product> products,
|
List<Product> products,
|
||||||
)? categoryListBuilder;
|
)? categoryListBuilder;
|
||||||
|
|
||||||
|
/// Builder for the list of selected categories
|
||||||
|
final Widget Function(ProductPageConfiguration configuration)?
|
||||||
|
selectedCategoryBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onProductDetail(
|
Future<void> _onProductDetail(
|
||||||
|
|
|
@ -142,6 +142,12 @@ class _ProductPage extends StatelessWidget {
|
||||||
shops: shops,
|
shops: shops,
|
||||||
onTap: configuration.shoppingService.shopService.selectShop,
|
onTap: configuration.shoppingService.shopService.selectShop,
|
||||||
),
|
),
|
||||||
|
configuration.selectedCategoryBuilder?.call(
|
||||||
|
configuration,
|
||||||
|
) ??
|
||||||
|
SelectedCategories(
|
||||||
|
configuration: configuration,
|
||||||
|
),
|
||||||
_ShopContents(
|
_ShopContents(
|
||||||
configuration: configuration,
|
configuration: configuration,
|
||||||
),
|
),
|
||||||
|
@ -203,31 +209,16 @@ class _ShopContents extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var productPageContent = snapshot.data;
|
List<Product> productPageContent;
|
||||||
|
|
||||||
if (productPageContent == null || productPageContent.isEmpty) {
|
productPageContent =
|
||||||
|
configuration.shoppingService.productService.products;
|
||||||
|
|
||||||
|
if (productPageContent.isEmpty) {
|
||||||
return configuration.noContentBuilder?.call(context) ??
|
return configuration.noContentBuilder?.call(context) ??
|
||||||
const DefaultNoContent();
|
const DefaultNoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
var productList = Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
// Products
|
|
||||||
|
|
||||||
getCategoryList(
|
|
||||||
context,
|
|
||||||
configuration,
|
|
||||||
productPageContent,
|
|
||||||
),
|
|
||||||
|
|
||||||
// Bottom padding so the last product is not cut off
|
|
||||||
// by the to shopping cart button.
|
|
||||||
const SizedBox(height: 48),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
var discountedproducts = productPageContent
|
var discountedproducts = productPageContent
|
||||||
.where((product) => product.hasDiscount)
|
.where((product) => product.hasDiscount)
|
||||||
.toList();
|
.toList();
|
||||||
|
@ -264,7 +255,29 @@ class _ShopContents extends StatelessWidget {
|
||||||
configuration,
|
configuration,
|
||||||
productPageContent,
|
productPageContent,
|
||||||
) ??
|
) ??
|
||||||
productList,
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Products
|
||||||
|
|
||||||
|
ListenableBuilder(
|
||||||
|
listenable:
|
||||||
|
configuration.shoppingService.productService,
|
||||||
|
builder: (context, _) => getCategoryList(
|
||||||
|
context,
|
||||||
|
configuration,
|
||||||
|
configuration
|
||||||
|
.shoppingService.productService.products,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Bottom padding so the last product is not cut off
|
||||||
|
// by the to shopping cart button.
|
||||||
|
const SizedBox(height: 48),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -272,3 +285,55 @@ class _ShopContents extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Selected categories.
|
||||||
|
class SelectedCategories extends StatelessWidget {
|
||||||
|
/// Constructor for the selected categories.
|
||||||
|
const SelectedCategories({
|
||||||
|
required this.configuration,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Configuration for the product page.
|
||||||
|
final ProductPageConfiguration configuration;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
return ListenableBuilder(
|
||||||
|
listenable: configuration.shoppingService.productService,
|
||||||
|
builder: (context, _) => Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 4),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
for (var category in configuration
|
||||||
|
.shoppingService.productService.selectedCategories) ...[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8),
|
||||||
|
child: Chip(
|
||||||
|
backgroundColor: theme.colorScheme.primary,
|
||||||
|
deleteIcon: const Icon(
|
||||||
|
Icons.close,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
onDeleted: () {
|
||||||
|
configuration.shoppingService.productService
|
||||||
|
.selectCategory(category);
|
||||||
|
},
|
||||||
|
label: Text(
|
||||||
|
category,
|
||||||
|
style: theme.textTheme.bodyMedium
|
||||||
|
?.copyWith(color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_product_page/flutter_product_page.dart";
|
import "package:flutter_product_page/flutter_product_page.dart";
|
||||||
|
import "package:flutter_product_page/src/category_selection_screen.dart";
|
||||||
|
|
||||||
/// Default appbar for the product page.
|
/// Default appbar for the product page.
|
||||||
class DefaultAppbar extends StatelessWidget implements PreferredSizeWidget {
|
class DefaultAppbar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
|
@ -19,7 +20,18 @@ class DefaultAppbar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
return AppBar(
|
return AppBar(
|
||||||
leading: IconButton(onPressed: () {}, icon: const Icon(Icons.person)),
|
leading: IconButton(onPressed: () {}, icon: const Icon(Icons.person)),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(onPressed: () {}, icon: const Icon(Icons.filter_alt)),
|
IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => CategorySelectionScreen(
|
||||||
|
configuration: configuration,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.filter_alt),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
title: Text(
|
title: Text(
|
||||||
configuration.translations.appBarTitle,
|
configuration.translations.appBarTitle,
|
||||||
|
|
|
@ -29,6 +29,7 @@ class ShoppingConfiguration {
|
||||||
this.categoryListBuilder,
|
this.categoryListBuilder,
|
||||||
this.shopselectorBuilder,
|
this.shopselectorBuilder,
|
||||||
this.discountBuilder,
|
this.discountBuilder,
|
||||||
|
this.selectedCategoryBuilder,
|
||||||
|
|
||||||
/// ShoppingCart configurations
|
/// ShoppingCart configurations
|
||||||
this.onConfirmOrder,
|
this.onConfirmOrder,
|
||||||
|
@ -57,6 +58,10 @@ class ShoppingConfiguration {
|
||||||
/// The service that will be used for the userstory
|
/// The service that will be used for the userstory
|
||||||
final ShoppingService shoppingService;
|
final ShoppingService shoppingService;
|
||||||
|
|
||||||
|
/// Builder for the list of selected categories
|
||||||
|
final Widget Function(ProductPageConfiguration configuration)?
|
||||||
|
selectedCategoryBuilder;
|
||||||
|
|
||||||
/// Function that will be called when the products are requested
|
/// Function that will be called when the products are requested
|
||||||
final Future<List<Product>> Function(String shopId)? onGetProducts;
|
final Future<List<Product>> Function(String shopId)? onGetProducts;
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,7 @@ class ShoppingProductPage extends StatelessWidget {
|
||||||
shopselectorBuilder: shoppingConfiguration.shopselectorBuilder,
|
shopselectorBuilder: shoppingConfiguration.shopselectorBuilder,
|
||||||
discountBuilder: shoppingConfiguration.discountBuilder,
|
discountBuilder: shoppingConfiguration.discountBuilder,
|
||||||
categoryListBuilder: shoppingConfiguration.categoryListBuilder,
|
categoryListBuilder: shoppingConfiguration.categoryListBuilder,
|
||||||
|
selectedCategoryBuilder: shoppingConfiguration.selectedCategoryBuilder,
|
||||||
shops: () async {
|
shops: () async {
|
||||||
if (shoppingConfiguration.onGetShops != null) {
|
if (shoppingConfiguration.onGetShops != null) {
|
||||||
return shoppingConfiguration.onGetShops!();
|
return shoppingConfiguration.onGetShops!();
|
||||||
|
|
|
@ -14,4 +14,10 @@ abstract class ProductService with ChangeNotifier {
|
||||||
|
|
||||||
/// Get current Products
|
/// Get current Products
|
||||||
List<Product> get products;
|
List<Product> get products;
|
||||||
|
|
||||||
|
/// Get current Products
|
||||||
|
List<String> get selectedCategories;
|
||||||
|
|
||||||
|
/// Select a category
|
||||||
|
void selectCategory(String category);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,12 @@ import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
|
||||||
/// Local product service
|
/// Local product service
|
||||||
class LocalProductService with ChangeNotifier implements ProductService {
|
class LocalProductService with ChangeNotifier implements ProductService {
|
||||||
List<Product> _products = [];
|
List<Product> _products = [];
|
||||||
|
List<Product> _allProducts = [];
|
||||||
|
final List<String> _selectedCategories = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<String> getCategories() =>
|
List<String> getCategories() =>
|
||||||
_products.map((e) => e.category).toSet().toList();
|
_allProducts.map((e) => e.category).toSet().toList();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Product> getProduct(String id) =>
|
Future<Product> getProduct(String id) =>
|
||||||
|
@ -60,9 +62,32 @@ class LocalProductService with ChangeNotifier implements ProductService {
|
||||||
description: "This is a delicious Brown fish",
|
description: "This is a delicious Brown fish",
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
_allProducts = List.from(_products);
|
||||||
return Future.value(_products);
|
return Future.value(_products);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Product> get products => _products;
|
List<Product> get products => _products;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void selectCategory(String category) {
|
||||||
|
if (_selectedCategories.contains(category)) {
|
||||||
|
_selectedCategories.remove(category);
|
||||||
|
} else {
|
||||||
|
_selectedCategories.add(category);
|
||||||
|
}
|
||||||
|
if (_selectedCategories.isEmpty) {
|
||||||
|
_products = List.from(_allProducts);
|
||||||
|
}
|
||||||
|
_products = _allProducts.where((element) {
|
||||||
|
if (_selectedCategories.isEmpty) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return _selectedCategories.contains(element.category);
|
||||||
|
}).toList();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String> get selectedCategories => _selectedCategories;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue