blob: 7f7805274cc537bac4288ce55e6f176f2c6ff855 [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_gen/gen_l10n/gallery_localizations.dart';
import 'package:gallery/data/gallery_options.dart';
import 'package:gallery/layout/adaptive.dart';
import 'package:gallery/studies/shrine/backdrop.dart';
import 'package:gallery/studies/shrine/category_menu_page.dart';
import 'package:gallery/studies/shrine/expanding_bottom_sheet.dart';
import 'package:gallery/studies/shrine/home.dart';
import 'package:gallery/studies/shrine/login.dart';
import 'package:gallery/studies/shrine/model/app_state_model.dart';
import 'package:gallery/studies/shrine/model/product.dart';
import 'package:gallery/studies/shrine/page_status.dart';
import 'package:gallery/studies/shrine/routes.dart' as routes;
import 'package:gallery/studies/shrine/scrim.dart';
import 'package:gallery/studies/shrine/supplemental/layout_cache.dart';
import 'package:gallery/studies/shrine/theme.dart';
import 'package:scoped_model/scoped_model.dart';
class ShrineApp extends StatefulWidget {
const ShrineApp({super.key});
static const String loginRoute = routes.loginRoute;
static const String homeRoute = routes.homeRoute;
@override
State<ShrineApp> createState() => _ShrineAppState();
}
class _ShrineAppState extends State<ShrineApp>
with TickerProviderStateMixin, RestorationMixin {
// Controller to coordinate both the opening/closing of backdrop and sliding
// of expanding bottom sheet
late AnimationController _controller;
// Animation Controller for expanding/collapsing the cart menu.
late AnimationController _expandingController;
final _RestorableAppStateModel _model = _RestorableAppStateModel();
final RestorableDouble _expandingTabIndex = RestorableDouble(0);
final RestorableDouble _tabIndex = RestorableDouble(1);
final Map<String, List<List<int>>> _layouts = {};
@override
String get restorationId => 'shrine_app_state';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_model, 'app_state_model');
registerForRestoration(_tabIndex, 'tab_index');
registerForRestoration(
_expandingTabIndex,
'expanding_tab_index',
);
_controller.value = _tabIndex.value;
_expandingController.value = _expandingTabIndex.value;
}
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 450),
value: 1,
);
// Save state restoration animation values only when the cart page
// fully opens or closes.
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed ||
status == AnimationStatus.dismissed) {
_tabIndex.value = _controller.value;
}
});
_expandingController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
// Save state restoration animation values only when the menu page
// fully opens or closes.
_expandingController.addStatusListener((status) {
if (status == AnimationStatus.completed ||
status == AnimationStatus.dismissed) {
_expandingTabIndex.value = _expandingController.value;
}
});
}
@override
void dispose() {
_controller.dispose();
_expandingController.dispose();
_tabIndex.dispose();
_expandingTabIndex.dispose();
super.dispose();
}
Widget mobileBackdrop() {
return Backdrop(
frontLayer: const ProductPage(),
backLayer: CategoryMenuPage(onCategoryTap: () => _controller.forward()),
frontTitle: const Text('SHRINE'),
backTitle: Text(GalleryLocalizations.of(context)!.shrineMenuCaption),
controller: _controller,
);
}
Widget desktopBackdrop() {
return const DesktopBackdrop(
frontLayer: ProductPage(),
backLayer: CategoryMenuPage(),
);
}
// Closes the bottom sheet if it is open.
Future<bool> _onWillPop() async {
final status = _expandingController.status;
if (status == AnimationStatus.completed ||
status == AnimationStatus.forward) {
await _expandingController.reverse();
return false;
}
return true;
}
@override
Widget build(BuildContext context) {
final Widget home = LayoutCache(
layouts: _layouts,
child: PageStatus(
menuController: _controller,
cartController: _expandingController,
child: LayoutBuilder(
builder: (context, constraints) => HomePage(
backdrop: isDisplayDesktop(context)
? desktopBackdrop()
: mobileBackdrop(),
scrim: Scrim(controller: _expandingController),
expandingBottomSheet: ExpandingBottomSheet(
hideController: _controller,
expandingController: _expandingController,
),
),
),
),
);
return ScopedModel<AppStateModel>(
model: _model.value,
child: WillPopScope(
onWillPop: _onWillPop,
child: MaterialApp(
// By default on desktop, scrollbars are applied by the
// ScrollBehavior. This overrides that. All vertical scrollables in
// the gallery need to be audited before enabling this feature,
// see https://github.com/flutter/gallery/issues/541
scrollBehavior:
const MaterialScrollBehavior().copyWith(scrollbars: false),
restorationScopeId: 'shrineApp',
title: 'Shrine',
debugShowCheckedModeBanner: false,
initialRoute: ShrineApp.loginRoute,
routes: {
ShrineApp.loginRoute: (context) => const LoginPage(),
ShrineApp.homeRoute: (context) => home,
},
theme: shrineTheme.copyWith(
platform: GalleryOptions.of(context).platform,
),
// L10n settings.
localizationsDelegates: GalleryLocalizations.localizationsDelegates,
supportedLocales: GalleryLocalizations.supportedLocales,
locale: GalleryOptions.of(context).locale,
),
),
);
}
}
class _RestorableAppStateModel extends RestorableListenable<AppStateModel> {
@override
AppStateModel createDefaultValue() => AppStateModel()..loadProducts();
@override
AppStateModel fromPrimitives(Object? data) {
final appState = AppStateModel()..loadProducts();
final appData = Map<String, dynamic>.from(data as Map);
// Reset selected category.
final categoryIndex = appData['category_index'] as int;
appState.setCategory(categories[categoryIndex]);
// Reset cart items.
final cartItems = appData['cart_data'] as Map<dynamic, dynamic>;
cartItems.forEach((dynamic id, dynamic quantity) {
appState.addMultipleProductsToCart(id as int, quantity as int);
});
return appState;
}
@override
Object toPrimitives() {
return <String, dynamic>{
'cart_data': value.productsInCart,
'category_index': categories.indexOf(value.selectedCategory),
};
}
}