// Copyright 2014 The Flutter Authors. 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:animations/animations.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:nested/nested.dart'; import 'package:provider/provider.dart'; import '../../data/gallery_options.dart'; import '../../gallery_localizations.dart'; import '../../layout/letter_spacing.dart'; import 'adaptive_nav.dart'; import 'colors.dart'; import 'compose_page.dart'; import 'model/email_model.dart'; import 'model/email_store.dart'; import 'routes.dart' as routes; final GlobalKey rootNavKey = GlobalKey(); class ReplyApp extends StatefulWidget { const ReplyApp({super.key}); static const String homeRoute = routes.homeRoute; static const String composeRoute = routes.composeRoute; static Route createComposeRoute(RouteSettings settings) { return PageRouteBuilder( pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) => const ComposePage(), transitionsBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { return FadeThroughTransition( fillColor: Theme.of(context).cardColor, animation: animation, secondaryAnimation: secondaryAnimation, child: child, ); }, settings: settings, ); } @override State createState() => _ReplyAppState(); } class _ReplyAppState extends State with RestorationMixin { final _RestorableEmailState _appState = _RestorableEmailState(); @override String get restorationId => 'replyState'; @override void restoreState(RestorationBucket? oldBucket, bool initialRestore) { registerForRestoration(_appState, 'state'); } @override void dispose() { _appState.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final ThemeMode galleryThemeMode = GalleryOptions.of(context).themeMode; final bool isDark = galleryThemeMode == ThemeMode.system ? Theme.of(context).brightness == Brightness.dark : galleryThemeMode == ThemeMode.dark; final ThemeData replyTheme = isDark ? _buildReplyDarkTheme(context) : _buildReplyLightTheme(context); return MultiProvider( providers: [ ChangeNotifierProvider.value( value: _appState.value, ), ], child: MaterialApp( navigatorKey: rootNavKey, restorationScopeId: 'appNavigator', title: 'Reply', debugShowCheckedModeBanner: false, theme: replyTheme, localizationsDelegates: GalleryLocalizations.localizationsDelegates, supportedLocales: GalleryLocalizations.supportedLocales, locale: GalleryOptions.of(context).locale, initialRoute: ReplyApp.homeRoute, onGenerateRoute: (RouteSettings settings) => switch (settings.name) { ReplyApp.homeRoute => MaterialPageRoute( builder: (BuildContext context) => const AdaptiveNav(), settings: settings, ), ReplyApp.composeRoute => ReplyApp.createComposeRoute(settings), _ => null, }, ), ); } } class _RestorableEmailState extends RestorableListenable { @override EmailStore createDefaultValue() { return EmailStore(); } @override EmailStore fromPrimitives(Object? data) { final EmailStore appState = EmailStore(); final Map appData = Map.from(data! as Map); appState.selectedEmailId = appData['selectedEmailId'] as int; appState.onSearchPage = appData['onSearchPage'] as bool; // The index of the MailboxPageType enum is restored. final int mailboxPageIndex = appData['selectedMailboxPage'] as int; appState.selectedMailboxPage = MailboxPageType.values[mailboxPageIndex]; final List starredEmailIdsList = appData['starredEmailIds'] as List; appState.starredEmailIds = { ...starredEmailIdsList.map((dynamic id) => id as int), }; final List trashEmailIdsList = appData['trashEmailIds'] as List; appState.trashEmailIds = { ...trashEmailIdsList.map((dynamic id) => id as int), }; return appState; } @override Object toPrimitives() { return { 'selectedEmailId': value.selectedEmailId, // The index of the MailboxPageType enum is stored, since the value // has to be serializable. 'selectedMailboxPage': value.selectedMailboxPage.index, 'onSearchPage': value.onSearchPage, 'starredEmailIds': value.starredEmailIds.toList(), 'trashEmailIds': value.trashEmailIds.toList(), }; } } ThemeData _buildReplyLightTheme(BuildContext context) { final ThemeData base = ThemeData.light(); return base.copyWith( bottomAppBarTheme: const BottomAppBarTheme(color: ReplyColors.blue700), bottomSheetTheme: BottomSheetThemeData( backgroundColor: ReplyColors.blue700, modalBackgroundColor: Colors.white.withOpacity(0.7), ), navigationRailTheme: NavigationRailThemeData( backgroundColor: ReplyColors.blue700, selectedIconTheme: const IconThemeData(color: ReplyColors.orange500), selectedLabelTextStyle: GoogleFonts.workSansTextTheme().headlineSmall!.copyWith( color: ReplyColors.orange500, ), unselectedIconTheme: const IconThemeData(color: ReplyColors.blue200), unselectedLabelTextStyle: GoogleFonts.workSansTextTheme().headlineSmall!.copyWith( color: ReplyColors.blue200, ), ), canvasColor: ReplyColors.white50, cardColor: ReplyColors.white50, chipTheme: _buildChipTheme( ReplyColors.blue700, ReplyColors.lightChipBackground, Brightness.light, ), colorScheme: const ColorScheme.light( primary: ReplyColors.blue700, primaryContainer: ReplyColors.blue800, secondary: ReplyColors.orange500, secondaryContainer: ReplyColors.orange400, error: ReplyColors.red400, onError: ReplyColors.black900, background: ReplyColors.blue50, ), textTheme: _buildReplyLightTextTheme(base.textTheme), scaffoldBackgroundColor: ReplyColors.blue50, ); } ThemeData _buildReplyDarkTheme(BuildContext context) { final ThemeData base = ThemeData.dark(); return base.copyWith( bottomAppBarTheme: const BottomAppBarTheme( color: ReplyColors.darkBottomAppBarBackground, ), bottomSheetTheme: BottomSheetThemeData( backgroundColor: ReplyColors.darkDrawerBackground, modalBackgroundColor: Colors.black.withOpacity(0.7), ), navigationRailTheme: NavigationRailThemeData( backgroundColor: ReplyColors.darkBottomAppBarBackground, selectedIconTheme: const IconThemeData(color: ReplyColors.orange300), selectedLabelTextStyle: GoogleFonts.workSansTextTheme().headlineSmall!.copyWith( color: ReplyColors.orange300, ), unselectedIconTheme: const IconThemeData(color: ReplyColors.greyLabel), unselectedLabelTextStyle: GoogleFonts.workSansTextTheme().headlineSmall!.copyWith( color: ReplyColors.greyLabel, ), ), canvasColor: ReplyColors.black900, cardColor: ReplyColors.darkCardBackground, chipTheme: _buildChipTheme( ReplyColors.blue200, ReplyColors.darkChipBackground, Brightness.dark, ), colorScheme: const ColorScheme.dark( primary: ReplyColors.blue200, primaryContainer: ReplyColors.blue300, secondary: ReplyColors.orange300, secondaryContainer: ReplyColors.orange300, error: ReplyColors.red200, background: ReplyColors.black900Alpha087, ), textTheme: _buildReplyDarkTextTheme(base.textTheme), scaffoldBackgroundColor: ReplyColors.black900, ); } ChipThemeData _buildChipTheme( Color primaryColor, Color chipBackground, Brightness brightness, ) { return ChipThemeData( backgroundColor: primaryColor.withOpacity(0.12), disabledColor: primaryColor.withOpacity(0.87), selectedColor: primaryColor.withOpacity(0.05), secondarySelectedColor: chipBackground, padding: const EdgeInsets.all(4), shape: const StadiumBorder(), labelStyle: GoogleFonts.workSansTextTheme().bodyMedium!.copyWith( color: brightness == Brightness.dark ? ReplyColors.white50 : ReplyColors.black900, ), secondaryLabelStyle: GoogleFonts.workSansTextTheme().bodyMedium, brightness: brightness, ); } TextTheme _buildReplyLightTextTheme(TextTheme base) { return base.copyWith( headlineMedium: GoogleFonts.workSans( fontWeight: FontWeight.w600, fontSize: 34, letterSpacing: letterSpacingOrNone(0.4), height: 0.9, color: ReplyColors.black900, ), headlineSmall: GoogleFonts.workSans( fontWeight: FontWeight.bold, fontSize: 24, letterSpacing: letterSpacingOrNone(0.27), color: ReplyColors.black900, ), titleLarge: GoogleFonts.workSans( fontWeight: FontWeight.w600, fontSize: 20, letterSpacing: letterSpacingOrNone(0.18), color: ReplyColors.black900, ), titleSmall: GoogleFonts.workSans( fontWeight: FontWeight.w600, fontSize: 14, letterSpacing: letterSpacingOrNone(-0.04), color: ReplyColors.black900, ), bodyLarge: GoogleFonts.workSans( fontWeight: FontWeight.normal, fontSize: 18, letterSpacing: letterSpacingOrNone(0.2), color: ReplyColors.black900, ), bodyMedium: GoogleFonts.workSans( fontWeight: FontWeight.normal, fontSize: 14, letterSpacing: letterSpacingOrNone(-0.05), color: ReplyColors.black900, ), bodySmall: GoogleFonts.workSans( fontWeight: FontWeight.normal, fontSize: 12, letterSpacing: letterSpacingOrNone(0.2), color: ReplyColors.black900, ), ); } TextTheme _buildReplyDarkTextTheme(TextTheme base) { return base.copyWith( headlineMedium: GoogleFonts.workSans( fontWeight: FontWeight.w600, fontSize: 34, letterSpacing: letterSpacingOrNone(0.4), height: 0.9, color: ReplyColors.white50, ), headlineSmall: GoogleFonts.workSans( fontWeight: FontWeight.bold, fontSize: 24, letterSpacing: letterSpacingOrNone(0.27), color: ReplyColors.white50, ), titleLarge: GoogleFonts.workSans( fontWeight: FontWeight.w600, fontSize: 20, letterSpacing: letterSpacingOrNone(0.18), color: ReplyColors.white50, ), titleSmall: GoogleFonts.workSans( fontWeight: FontWeight.w600, fontSize: 14, letterSpacing: letterSpacingOrNone(-0.04), color: ReplyColors.white50, ), bodyLarge: GoogleFonts.workSans( fontWeight: FontWeight.normal, fontSize: 18, letterSpacing: letterSpacingOrNone(0.2), color: ReplyColors.white50, ), bodyMedium: GoogleFonts.workSans( fontWeight: FontWeight.normal, fontSize: 14, letterSpacing: letterSpacingOrNone(-0.05), color: ReplyColors.white50, ), bodySmall: GoogleFonts.workSans( fontWeight: FontWeight.normal, fontSize: 12, letterSpacing: letterSpacingOrNone(0.2), color: ReplyColors.white50, ), ); }