blob: 3f00c93a1fabe388d9ed0e1f8fccb340f2cab78b [file] [log] [blame]
// Copyright 2019 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_gen/gen_l10n/gallery_localizations.dart';
import 'package:gallery/layout/letter_spacing.dart';
import 'package:gallery/studies/shrine/colors.dart';
import 'package:gallery/studies/shrine/expanding_bottom_sheet.dart';
import 'package:gallery/studies/shrine/model/app_state_model.dart';
import 'package:gallery/studies/shrine/model/product.dart';
import 'package:gallery/studies/shrine/theme.dart';
import 'package:intl/intl.dart';
import 'package:scoped_model/scoped_model.dart';
const _startColumnWidth = 60.0;
const _ordinalSortKeyName = 'shopping_cart';
class ShoppingCartPage extends StatefulWidget {
const ShoppingCartPage({super.key});
@override
State<ShoppingCartPage> createState() => _ShoppingCartPageState();
}
class _ShoppingCartPageState extends State<ShoppingCartPage> {
List<Widget> _createShoppingCartRows(AppStateModel model) {
return model.productsInCart.keys
.map(
(id) => ShoppingCartRow(
product: model.getProductById(id),
quantity: model.productsInCart[id],
onPressed: () {
model.removeItemFromCart(id);
},
),
)
.toList();
}
@override
Widget build(BuildContext context) {
final localTheme = Theme.of(context);
return Scaffold(
backgroundColor: shrinePink50,
body: SafeArea(
child: ScopedModelDescendant<AppStateModel>(
builder: (context, child, model) {
final localizations = GalleryLocalizations.of(context)!;
final expandingBottomSheet = ExpandingBottomSheet.of(context);
return Stack(
children: [
ListView(
children: [
Semantics(
sortKey:
const OrdinalSortKey(0, name: _ordinalSortKeyName),
child: Row(
children: [
SizedBox(
width: _startColumnWidth,
child: IconButton(
icon: const Icon(Icons.keyboard_arrow_down),
onPressed: () => expandingBottomSheet!.close(),
tooltip: localizations.shrineTooltipCloseCart,
),
),
Text(
localizations.shrineCartPageCaption,
style: localTheme.textTheme.titleMedium!
.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(width: 16),
Text(
localizations.shrineCartItemCount(
model.totalCartQuantity,
),
),
],
),
),
const SizedBox(height: 16),
Semantics(
sortKey:
const OrdinalSortKey(1, name: _ordinalSortKeyName),
child: Column(
children: _createShoppingCartRows(model),
),
),
Semantics(
sortKey:
const OrdinalSortKey(2, name: _ordinalSortKeyName),
child: ShoppingCartSummary(model: model),
),
const SizedBox(height: 100),
],
),
PositionedDirectional(
bottom: 16,
start: 16,
end: 16,
child: Semantics(
sortKey: const OrdinalSortKey(3, name: _ordinalSortKeyName),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: const BeveledRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(7)),
),
backgroundColor: shrinePink100,
),
onPressed: () {
model.clearCart();
expandingBottomSheet!.close();
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(
localizations.shrineCartClearButtonCaption,
style: TextStyle(
letterSpacing:
letterSpacingOrNone(largeLetterSpacing)),
),
),
),
),
),
],
);
},
),
),
);
}
}
class ShoppingCartSummary extends StatelessWidget {
const ShoppingCartSummary({
super.key,
required this.model,
});
final AppStateModel model;
@override
Widget build(BuildContext context) {
final smallAmountStyle =
Theme.of(context).textTheme.bodyMedium!.copyWith(color: shrineBrown600);
final largeAmountStyle = Theme.of(context)
.textTheme
.headlineMedium!
.copyWith(letterSpacing: letterSpacingOrNone(mediumLetterSpacing));
final formatter = NumberFormat.simpleCurrency(
decimalDigits: 2,
locale: Localizations.localeOf(context).toString(),
);
final localizations = GalleryLocalizations.of(context)!;
return Row(
children: [
const SizedBox(width: _startColumnWidth),
Expanded(
child: Padding(
padding: const EdgeInsetsDirectional.only(end: 16),
child: Column(
children: [
MergeSemantics(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SelectableText(
localizations.shrineCartTotalCaption,
),
Expanded(
child: SelectableText(
formatter.format(model.totalCost),
style: largeAmountStyle,
textAlign: TextAlign.end,
),
),
],
),
),
const SizedBox(height: 16),
MergeSemantics(
child: Row(
children: [
SelectableText(
localizations.shrineCartSubtotalCaption,
),
Expanded(
child: SelectableText(
formatter.format(model.subtotalCost),
style: smallAmountStyle,
textAlign: TextAlign.end,
),
),
],
),
),
const SizedBox(height: 4),
MergeSemantics(
child: Row(
children: [
SelectableText(
localizations.shrineCartShippingCaption,
),
Expanded(
child: SelectableText(
formatter.format(model.shippingCost),
style: smallAmountStyle,
textAlign: TextAlign.end,
),
),
],
),
),
const SizedBox(height: 4),
MergeSemantics(
child: Row(
children: [
SelectableText(
localizations.shrineCartTaxCaption,
),
Expanded(
child: SelectableText(
formatter.format(model.tax),
style: smallAmountStyle,
textAlign: TextAlign.end,
),
),
],
),
),
],
),
),
),
],
);
}
}
class ShoppingCartRow extends StatelessWidget {
const ShoppingCartRow({
super.key,
required this.product,
required this.quantity,
this.onPressed,
});
final Product product;
final int? quantity;
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
final formatter = NumberFormat.simpleCurrency(
decimalDigits: 0,
locale: Localizations.localeOf(context).toString(),
);
final localTheme = Theme.of(context);
final localizations = GalleryLocalizations.of(context)!;
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
key: ValueKey<int>(product.id),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Semantics(
container: true,
label: localizations
.shrineScreenReaderRemoveProductButton(product.name(context)),
button: true,
enabled: true,
child: ExcludeSemantics(
child: SizedBox(
width: _startColumnWidth,
child: IconButton(
icon: const Icon(Icons.remove_circle_outline),
onPressed: onPressed,
tooltip: localizations.shrineTooltipRemoveItem,
),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsetsDirectional.only(end: 16),
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(
product.assetName,
package: product.assetPackage,
fit: BoxFit.cover,
width: 75,
height: 75,
excludeFromSemantics: true,
),
const SizedBox(width: 16),
Expanded(
child: MergeSemantics(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MergeSemantics(
child: Row(
children: [
Expanded(
child: SelectableText(
localizations
.shrineProductQuantity(quantity!),
),
),
SelectableText(
localizations.shrineProductPrice(
formatter.format(product.price),
),
),
],
),
),
SelectableText(
product.name(context),
style: localTheme.textTheme.titleMedium!
.copyWith(fontWeight: FontWeight.w600),
),
],
),
),
),
],
),
const SizedBox(height: 16),
const Divider(
color: shrineBrown900,
height: 10,
),
],
),
),
),
],
),
);
}
}