diff --git a/dev/integration_tests/new_gallery/README.md b/dev/integration_tests/new_gallery/README.md new file mode 100644 index 0000000000..f093ebbc12 --- /dev/null +++ b/dev/integration_tests/new_gallery/README.md @@ -0,0 +1,29 @@ +# Flutter Gallery + +**NOTE**: The Flutter Gallery is now deprecated, and no longer being active maintained. + +Flutter Gallery was a resource to help developers evaluate and use Flutter. +It is now being used primarily for testing. For posterity, the web version +remains [hosted here](https://flutter-gallery-archive.web.app). + +We recommend Flutter developers check out the following resources: + +* **Wonderous** +([web demo](https://wonderous.app/web/), +[App Store](https://apps.apple.com/us/app/wonderous/id1612491897), +[Google Play](https://play.google.com/store/apps/details?id=com.gskinner.flutter.wonders), +[source code](https://github.com/gskinnerTeam/flutter-wonderous-app)):
+A Flutter app that showcases Flutter's support for elegant design and rich animations. + +* **Material 3 Demo** +([web demo](https://flutter.github.io/samples/web/material_3_demo/), +[source code](https://github.com/flutter/samples/tree/main/material_3_demo)):
+A Flutter app that showcases Material 3 features in the Flutter Material library. + +* **Flutter Samples** +([samples](https://flutter.github.io/samples), [source code](https://github.com/flutter/samples)):
+A collection of open source samples that illustrate best practices for Flutter. + +* **Widget catalogs** +([Material](https://docs.flutter.dev/ui/widgets/material), [Cupertino](https://docs.flutter.dev/ui/widgets/cupertino)):
+Catalogs for Material, Cupertino, and other widgets available for use in UI. diff --git a/dev/integration_tests/new_gallery/lib/codeviewer/code_displayer.dart b/dev/integration_tests/new_gallery/lib/codeviewer/code_displayer.dart new file mode 100644 index 0000000000..fcfe2b121d --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/codeviewer/code_displayer.dart @@ -0,0 +1,7 @@ +// 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:flutter/material.dart'; + +typedef CodeDisplayer = TextSpan Function(BuildContext context); diff --git a/dev/integration_tests/new_gallery/lib/codeviewer/code_style.dart b/dev/integration_tests/new_gallery/lib/codeviewer/code_style.dart new file mode 100644 index 0000000000..8ca8d175b3 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/codeviewer/code_style.dart @@ -0,0 +1,44 @@ +// 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:flutter/material.dart'; + +class CodeStyle extends InheritedWidget { + const CodeStyle({ + super.key, + this.baseStyle, + this.numberStyle, + this.commentStyle, + this.keywordStyle, + this.stringStyle, + this.punctuationStyle, + this.classStyle, + this.constantStyle, + required super.child, + }); + + final TextStyle? baseStyle; + final TextStyle? numberStyle; + final TextStyle? commentStyle; + final TextStyle? keywordStyle; + final TextStyle? stringStyle; + final TextStyle? punctuationStyle; + final TextStyle? classStyle; + final TextStyle? constantStyle; + + static CodeStyle of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType()!; + } + + @override + bool updateShouldNotify(CodeStyle oldWidget) => + oldWidget.baseStyle != baseStyle || + oldWidget.numberStyle != numberStyle || + oldWidget.commentStyle != commentStyle || + oldWidget.keywordStyle != keywordStyle || + oldWidget.stringStyle != stringStyle || + oldWidget.punctuationStyle != punctuationStyle || + oldWidget.classStyle != classStyle || + oldWidget.constantStyle != constantStyle; +} diff --git a/dev/integration_tests/new_gallery/lib/constants.dart b/dev/integration_tests/new_gallery/lib/constants.dart new file mode 100644 index 0000000000..f9d1e1cde2 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/constants.dart @@ -0,0 +1,53 @@ +// 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. + +// Only put constants shared between files here. + +import 'dart:typed_data'; + +// Height of the 'Gallery' header +const double galleryHeaderHeight = 64; + +// The font size delta for headline4 font. +const double desktopDisplay1FontDelta = 16; + +// The width of the settingsDesktop. +const double desktopSettingsWidth = 520; + +// Sentinel value for the system text scale factor option. +const double systemTextScaleFactorOption = -1; + +// The splash page animation duration. +const Duration splashPageAnimationDuration = Duration(milliseconds: 300); + +// Half the splash page animation duration. +const Duration halfSplashPageAnimationDuration = Duration(milliseconds: 150); + +// Duration for settings panel to open on mobile. +const Duration settingsPanelMobileAnimationDuration = + Duration(milliseconds: 200); + +// Duration for settings panel to open on desktop. +const Duration settingsPanelDesktopAnimationDuration = + Duration(milliseconds: 600); + +// Duration for home page elements to fade in. +const Duration entranceAnimationDuration = Duration(milliseconds: 200); + +// The desktop top padding for a page's first header (e.g. Gallery, Settings) +const double firstHeaderDesktopTopPadding = 5.0; + +// A transparent image used to avoid loading images when they are not needed. +final Uint8List kTransparentImage = Uint8List.fromList([ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, + 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, + 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x06, 0x62, 0x4B, + 0x47, 0x44, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xA0, 0xBD, 0xA7, 0x93, 0x00, + 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0B, 0x13, 0x00, 0x00, + 0x0B, 0x13, 0x01, 0x00, 0x9A, 0x9C, 0x18, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, + 0x4D, 0x45, 0x07, 0xE6, 0x03, 0x10, 0x17, 0x07, 0x1D, 0x2E, 0x5E, 0x30, 0x9B, + 0x00, 0x00, 0x00, 0x0B, 0x49, 0x44, 0x41, 0x54, 0x08, 0xD7, 0x63, 0x60, 0x00, + 0x02, 0x00, 0x00, 0x05, 0x00, 0x01, 0xE2, 0x26, 0x05, 0x9B, 0x00, 0x00, 0x00, + 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, +]); diff --git a/dev/integration_tests/new_gallery/lib/data/demos.dart b/dev/integration_tests/new_gallery/lib/data/demos.dart new file mode 100644 index 0000000000..48d8049ae2 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/data/demos.dart @@ -0,0 +1,1323 @@ +// 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 'dart:collection'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import '../codeviewer/code_displayer.dart'; +import '../deferred_widget.dart'; +import '../demos/cupertino/cupertino_demos.dart' + deferred as cupertino_demos; +import '../demos/cupertino/demo_types.dart'; +import '../demos/material/material_demo_types.dart'; +import '../demos/material/material_demos.dart' + deferred as material_demos; +import '../demos/reference/colors_demo.dart' + deferred as colors_demo; +import '../demos/reference/motion_demo_container_transition.dart' + deferred as motion_demo_container; +import '../demos/reference/motion_demo_fade_scale_transition.dart'; +import '../demos/reference/motion_demo_fade_through_transition.dart'; +import '../demos/reference/motion_demo_shared_x_axis_transition.dart'; +import '../demos/reference/motion_demo_shared_y_axis_transition.dart'; +import '../demos/reference/motion_demo_shared_z_axis_transition.dart'; +import '../demos/reference/transformations_demo.dart' + deferred as transformations_demo; +import '../demos/reference/two_pane_demo.dart' + deferred as twopane_demo; +import '../demos/reference/typography_demo.dart' + deferred as typography; +import '../gallery_localizations.dart'; +import '../gallery_localizations_en.dart'; +import 'icons.dart'; + +const String _docsBaseUrl = 'https://api.flutter.dev/flutter'; +const String _docsAnimationsUrl = + 'https://pub.dev/documentation/animations/latest/animations'; + +enum GalleryDemoCategory { + study, + material, + cupertino, + other; + + @override + String toString() { + return name.toUpperCase(); + } + + String? displayTitle(GalleryLocalizations localizations) { + switch (this) { + case GalleryDemoCategory.other: + return localizations.homeCategoryReference; + case GalleryDemoCategory.material: + case GalleryDemoCategory.cupertino: + return toString(); + case GalleryDemoCategory.study: + } + return null; + } +} + +class GalleryDemo { + const GalleryDemo({ + required this.title, + required this.category, + required this.subtitle, + // This parameter is required for studies. + this.studyId, + // Parameters below are required for non-study demos. + this.slug, + this.icon, + this.configurations = const [], + }) : assert(category == GalleryDemoCategory.study || + (slug != null && icon != null)), + assert(slug != null || studyId != null); + + final String title; + final GalleryDemoCategory category; + final String subtitle; + final String? studyId; + final String? slug; + final IconData? icon; + final List configurations; + + String get describe => '${slug ?? studyId}@${category.name}'; +} + +TextSpan noOpCodeDisplayer(BuildContext context) { + return const TextSpan(text: ''); +} + +class GalleryDemoConfiguration { + const GalleryDemoConfiguration({ + required this.title, + required this.description, + required this.documentationUrl, + required this.buildRoute, + this.code = noOpCodeDisplayer, + }); + + final String title; + final String description; + final String documentationUrl; + final WidgetBuilder buildRoute; + final CodeDisplayer code; +} + +/// Awaits all deferred libraries for tests. +Future pumpDeferredLibraries() { + final List> futures = >[ + DeferredWidget.preload(cupertino_demos.loadLibrary), + DeferredWidget.preload(material_demos.loadLibrary), + DeferredWidget.preload(motion_demo_container.loadLibrary), + DeferredWidget.preload(colors_demo.loadLibrary), + DeferredWidget.preload(transformations_demo.loadLibrary), + DeferredWidget.preload(typography.loadLibrary), + ]; + return Future.wait(futures); +} + +class Demos { + static Map asSlugToDemoMap(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return LinkedHashMap.fromIterable( + all(localizations), + // ignore: avoid_dynamic_calls + key: (dynamic demo) => demo.slug as String?, + ); + } + + static List all(GalleryLocalizations localizations) => + studies(localizations).values.toList() + + materialDemos(localizations) + + cupertinoDemos(localizations) + + otherDemos(localizations); + + static List allDescriptions() => + all(GalleryLocalizationsEn()).map((GalleryDemo demo) => demo.describe).toList(); + + static Map studies(GalleryLocalizations localizations) { + return { + 'shrine': GalleryDemo( + title: 'Shrine', + subtitle: localizations.shrineDescription, + category: GalleryDemoCategory.study, + studyId: 'shrine', + ), + 'rally': GalleryDemo( + title: 'Rally', + subtitle: localizations.rallyDescription, + category: GalleryDemoCategory.study, + studyId: 'rally', + ), + 'crane': GalleryDemo( + title: 'Crane', + subtitle: localizations.craneDescription, + category: GalleryDemoCategory.study, + studyId: 'crane', + ), + 'fortnightly': GalleryDemo( + title: 'Fortnightly', + subtitle: localizations.fortnightlyDescription, + category: GalleryDemoCategory.study, + studyId: 'fortnightly', + ), + 'reply': GalleryDemo( + title: 'Reply', + subtitle: localizations.replyDescription, + category: GalleryDemoCategory.study, + studyId: 'reply', + ), + 'starterApp': GalleryDemo( + title: localizations.starterAppTitle, + subtitle: localizations.starterAppDescription, + category: GalleryDemoCategory.study, + studyId: 'starter', + ), + }; + } + + static List materialDemos(GalleryLocalizations localizations) { + final LibraryLoader materialDemosLibrary = material_demos.loadLibrary; + return [ + GalleryDemo( + title: localizations.demoAppBarTitle, + icon: GalleryIcons.appbar, + slug: 'app-bar', + subtitle: localizations.demoAppBarSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoAppBarTitle, + description: localizations.demoAppBarDescription, + documentationUrl: '$_docsBaseUrl/material/AppBar-class.html', + buildRoute: (_) => DeferredWidget( + materialDemosLibrary, + () => material_demos.AppBarDemo(), + ), + ), + ], + category: GalleryDemoCategory.material, + ), + GalleryDemo( + title: localizations.demoBannerTitle, + icon: GalleryIcons.listsLeaveBehind, + slug: 'banner', + subtitle: localizations.demoBannerSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoBannerTitle, + description: localizations.demoBannerDescription, + documentationUrl: + '$_docsBaseUrl/material/MaterialBanner-class.html', + buildRoute: (_) => DeferredWidget( + materialDemosLibrary, + () => material_demos.BannerDemo(), + ), + ), + ], + category: GalleryDemoCategory.material, + ), + GalleryDemo( + title: localizations.demoBottomAppBarTitle, + icon: GalleryIcons.bottomAppBar, + slug: 'bottom-app-bar', + subtitle: localizations.demoBottomAppBarSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoBottomAppBarTitle, + description: localizations.demoBottomAppBarDescription, + documentationUrl: '$_docsBaseUrl/material/BottomAppBar-class.html', + buildRoute: (_) => DeferredWidget( + materialDemosLibrary, + () => material_demos.BottomAppBarDemo(), + ), + ), + ], + category: GalleryDemoCategory.material, + ), + GalleryDemo( + title: localizations.demoBottomNavigationTitle, + icon: GalleryIcons.bottomNavigation, + slug: 'bottom-navigation', + subtitle: localizations.demoBottomNavigationSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoBottomNavigationPersistentLabels, + description: localizations.demoBottomNavigationDescription, + documentationUrl: + '$_docsBaseUrl/material/BottomNavigationBar-class.html', + buildRoute: (_) => DeferredWidget( + materialDemosLibrary, + () => material_demos.BottomNavigationDemo( + type: BottomNavigationDemoType.withLabels, + restorationId: 'bottom_navigation_labels_demo', + )), + ), + GalleryDemoConfiguration( + title: localizations.demoBottomNavigationSelectedLabel, + description: localizations.demoBottomNavigationDescription, + documentationUrl: + '$_docsBaseUrl/material/BottomNavigationBar-class.html', + buildRoute: (_) => DeferredWidget( + materialDemosLibrary, + () => material_demos.BottomNavigationDemo( + type: BottomNavigationDemoType.withoutLabels, + restorationId: 'bottom_navigation_without_labels_demo', + )), + ), + ], + category: GalleryDemoCategory.material, + ), + GalleryDemo( + title: localizations.demoBottomSheetTitle, + icon: GalleryIcons.bottomSheets, + slug: 'bottom-sheet', + subtitle: localizations.demoBottomSheetSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoBottomSheetPersistentTitle, + description: localizations.demoBottomSheetPersistentDescription, + documentationUrl: '$_docsBaseUrl/material/BottomSheet-class.html', + buildRoute: (_) => DeferredWidget( + materialDemosLibrary, + () => material_demos.BottomSheetDemo( + type: BottomSheetDemoType.persistent, + )), + ), + GalleryDemoConfiguration( + title: localizations.demoBottomSheetModalTitle, + description: localizations.demoBottomSheetModalDescription, + documentationUrl: '$_docsBaseUrl/material/BottomSheet-class.html', + buildRoute: (_) => DeferredWidget( + materialDemosLibrary, + () => material_demos.BottomSheetDemo( + type: BottomSheetDemoType.modal, + )), + ), + ], + category: GalleryDemoCategory.material, + ), + GalleryDemo( + title: localizations.demoButtonTitle, + icon: GalleryIcons.genericButtons, + slug: 'button', + subtitle: localizations.demoButtonSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoTextButtonTitle, + description: localizations.demoTextButtonDescription, + documentationUrl: '$_docsBaseUrl/material/TextButton-class.html', + buildRoute: (_) => DeferredWidget(materialDemosLibrary, + () => material_demos.ButtonDemo(type: ButtonDemoType.text)), + ), + GalleryDemoConfiguration( + title: localizations.demoElevatedButtonTitle, + description: localizations.demoElevatedButtonDescription, + documentationUrl: + '$_docsBaseUrl/material/ElevatedButton-class.html', + buildRoute: (_) => DeferredWidget(materialDemosLibrary, + () => material_demos.ButtonDemo(type: ButtonDemoType.elevated)), + ), + GalleryDemoConfiguration( + title: localizations.demoOutlinedButtonTitle, + description: localizations.demoOutlinedButtonDescription, + documentationUrl: + '$_docsBaseUrl/material/OutlinedButton-class.html', + buildRoute: (_) => DeferredWidget(materialDemosLibrary, + () => material_demos.ButtonDemo(type: ButtonDemoType.outlined)), + ), + GalleryDemoConfiguration( + title: localizations.demoToggleButtonTitle, + description: localizations.demoToggleButtonDescription, + documentationUrl: '$_docsBaseUrl/material/ToggleButtons-class.html', + buildRoute: (_) => DeferredWidget(materialDemosLibrary, + () => material_demos.ButtonDemo(type: ButtonDemoType.toggle)), + ), + GalleryDemoConfiguration( + title: localizations.demoFloatingButtonTitle, + description: localizations.demoFloatingButtonDescription, + documentationUrl: + '$_docsBaseUrl/material/FloatingActionButton-class.html', + buildRoute: (_) => DeferredWidget(materialDemosLibrary, + () => material_demos.ButtonDemo(type: ButtonDemoType.floating)), + ), + ], + category: GalleryDemoCategory.material, + ), + GalleryDemo( + title: localizations.demoCardTitle, + icon: GalleryIcons.cards, + slug: 'card', + subtitle: localizations.demoCardSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoCardTitle, + description: localizations.demoCardDescription, + documentationUrl: '$_docsBaseUrl/material/Card-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.CardsDemo(), + ), + ), + ], + category: GalleryDemoCategory.material, + ), + GalleryDemo( + title: localizations.demoChipTitle, + icon: GalleryIcons.chips, + slug: 'chip', + subtitle: localizations.demoChipSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoActionChipTitle, + description: localizations.demoActionChipDescription, + documentationUrl: '$_docsBaseUrl/material/ActionChip-class.html', + buildRoute: (BuildContext context) => DeferredWidget(materialDemosLibrary, + () => material_demos.ChipDemo(type: ChipDemoType.action)), + ), + GalleryDemoConfiguration( + title: localizations.demoChoiceChipTitle, + description: localizations.demoChoiceChipDescription, + documentationUrl: '$_docsBaseUrl/material/ChoiceChip-class.html', + buildRoute: (BuildContext context) => DeferredWidget(materialDemosLibrary, + () => material_demos.ChipDemo(type: ChipDemoType.choice)), + ), + GalleryDemoConfiguration( + title: localizations.demoFilterChipTitle, + description: localizations.demoFilterChipDescription, + documentationUrl: '$_docsBaseUrl/material/FilterChip-class.html', + buildRoute: (BuildContext context) => DeferredWidget(materialDemosLibrary, + () => material_demos.ChipDemo(type: ChipDemoType.filter)), + ), + GalleryDemoConfiguration( + title: localizations.demoInputChipTitle, + description: localizations.demoInputChipDescription, + documentationUrl: '$_docsBaseUrl/material/InputChip-class.html', + buildRoute: (BuildContext context) => DeferredWidget(materialDemosLibrary, + () => material_demos.ChipDemo(type: ChipDemoType.input)), + ), + ], + category: GalleryDemoCategory.material, + ), + GalleryDemo( + title: localizations.demoDataTableTitle, + icon: GalleryIcons.dataTable, + slug: 'data-table', + subtitle: localizations.demoDataTableSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoDataTableTitle, + description: localizations.demoDataTableDescription, + documentationUrl: '$_docsBaseUrl/material/DataTable-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.DataTableDemo(), + ), + ), + ], + category: GalleryDemoCategory.material, + ), + GalleryDemo( + title: localizations.demoDialogTitle, + icon: GalleryIcons.dialogs, + slug: 'dialog', + subtitle: localizations.demoDialogSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoAlertDialogTitle, + description: localizations.demoAlertDialogDescription, + documentationUrl: '$_docsBaseUrl/material/AlertDialog-class.html', + buildRoute: (BuildContext context) => DeferredWidget(materialDemosLibrary, + () => material_demos.DialogDemo(type: DialogDemoType.alert)), + ), + GalleryDemoConfiguration( + title: localizations.demoAlertTitleDialogTitle, + description: localizations.demoAlertDialogDescription, + documentationUrl: '$_docsBaseUrl/material/AlertDialog-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => + material_demos.DialogDemo(type: DialogDemoType.alertTitle)), + ), + GalleryDemoConfiguration( + title: localizations.demoSimpleDialogTitle, + description: localizations.demoSimpleDialogDescription, + documentationUrl: '$_docsBaseUrl/material/SimpleDialog-class.html', + buildRoute: (BuildContext context) => DeferredWidget(materialDemosLibrary, + () => material_demos.DialogDemo(type: DialogDemoType.simple)), + ), + GalleryDemoConfiguration( + title: localizations.demoFullscreenDialogTitle, + description: localizations.demoFullscreenDialogDescription, + documentationUrl: + '$_docsBaseUrl/widgets/PageRoute/fullscreenDialog.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => + material_demos.DialogDemo(type: DialogDemoType.fullscreen)), + ), + ], + category: GalleryDemoCategory.material, + ), + GalleryDemo( + title: localizations.demoDividerTitle, + icon: GalleryIcons.divider, + slug: 'divider', + subtitle: localizations.demoDividerSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoDividerTitle, + description: localizations.demoDividerDescription, + documentationUrl: '$_docsBaseUrl/material/Divider-class.html', + buildRoute: (_) => DeferredWidget( + materialDemosLibrary, + () => material_demos.DividerDemo( + type: DividerDemoType.horizontal)), + ), + GalleryDemoConfiguration( + title: localizations.demoVerticalDividerTitle, + description: localizations.demoDividerDescription, + documentationUrl: + '$_docsBaseUrl/material/VerticalDivider-class.html', + buildRoute: (_) => DeferredWidget( + materialDemosLibrary, + () => + material_demos.DividerDemo(type: DividerDemoType.vertical)), + ), + ], + category: GalleryDemoCategory.material, + ), + GalleryDemo( + title: localizations.demoGridListsTitle, + icon: GalleryIcons.gridOn, + slug: 'grid-lists', + subtitle: localizations.demoGridListsSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoGridListsImageOnlyTitle, + description: localizations.demoGridListsDescription, + documentationUrl: '$_docsBaseUrl/widgets/GridView-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.GridListDemo( + type: GridListDemoType.imageOnly)), + ), + GalleryDemoConfiguration( + title: localizations.demoGridListsHeaderTitle, + description: localizations.demoGridListsDescription, + documentationUrl: '$_docsBaseUrl/widgets/GridView-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => + material_demos.GridListDemo(type: GridListDemoType.header)), + ), + GalleryDemoConfiguration( + title: localizations.demoGridListsFooterTitle, + description: localizations.demoGridListsDescription, + documentationUrl: '$_docsBaseUrl/widgets/GridView-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => + material_demos.GridListDemo(type: GridListDemoType.footer)), + ), + ], + category: GalleryDemoCategory.material, + ), + GalleryDemo( + title: localizations.demoListsTitle, + icon: GalleryIcons.listAlt, + slug: 'lists', + subtitle: localizations.demoListsSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoOneLineListsTitle, + description: localizations.demoListsDescription, + documentationUrl: '$_docsBaseUrl/material/ListTile-class.html', + buildRoute: (BuildContext context) => DeferredWidget(materialDemosLibrary, + () => material_demos.ListDemo(type: ListDemoType.oneLine)), + ), + GalleryDemoConfiguration( + title: localizations.demoTwoLineListsTitle, + description: localizations.demoListsDescription, + documentationUrl: '$_docsBaseUrl/material/ListTile-class.html', + buildRoute: (BuildContext context) => DeferredWidget(materialDemosLibrary, + () => material_demos.ListDemo(type: ListDemoType.twoLine)), + ), + ], + category: GalleryDemoCategory.material, + ), + GalleryDemo( + title: localizations.demoMenuTitle, + icon: GalleryIcons.moreVert, + slug: 'menu', + subtitle: localizations.demoMenuSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoContextMenuTitle, + description: localizations.demoMenuDescription, + documentationUrl: '$_docsBaseUrl/material/PopupMenuItem-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.MenuDemo(type: MenuDemoType.contextMenu), + ), + ), + GalleryDemoConfiguration( + title: localizations.demoSectionedMenuTitle, + description: localizations.demoMenuDescription, + documentationUrl: '$_docsBaseUrl/material/PopupMenuItem-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.MenuDemo(type: MenuDemoType.sectionedMenu), + ), + ), + GalleryDemoConfiguration( + title: localizations.demoChecklistMenuTitle, + description: localizations.demoMenuDescription, + documentationUrl: + '$_docsBaseUrl/material/CheckedPopupMenuItem-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.MenuDemo(type: MenuDemoType.checklistMenu), + ), + ), + GalleryDemoConfiguration( + title: localizations.demoSimpleMenuTitle, + description: localizations.demoMenuDescription, + documentationUrl: '$_docsBaseUrl/material/PopupMenuItem-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.MenuDemo(type: MenuDemoType.simpleMenu), + ), + ), + ], + category: GalleryDemoCategory.material, + ), + GalleryDemo( + title: localizations.demoNavigationDrawerTitle, + icon: GalleryIcons.menu, + slug: 'nav_drawer', + subtitle: localizations.demoNavigationDrawerSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoNavigationDrawerTitle, + description: localizations.demoNavigationDrawerDescription, + documentationUrl: '$_docsBaseUrl/material/Drawer-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.NavDrawerDemo(), + ), + ), + ], + category: GalleryDemoCategory.material, + ), + GalleryDemo( + title: localizations.demoNavigationRailTitle, + icon: GalleryIcons.navigationRail, + slug: 'nav_rail', + subtitle: localizations.demoNavigationRailSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoNavigationRailTitle, + description: localizations.demoNavigationRailDescription, + documentationUrl: + '$_docsBaseUrl/material/NavigationRail-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.NavRailDemo(), + ), + ), + ], + category: GalleryDemoCategory.material, + ), + GalleryDemo( + title: localizations.demoPickersTitle, + icon: GalleryIcons.event, + slug: 'pickers', + subtitle: localizations.demoPickersSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoDatePickerTitle, + description: localizations.demoDatePickerDescription, + documentationUrl: '$_docsBaseUrl/material/showDatePicker.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.PickerDemo(type: PickerDemoType.date), + ), + ), + GalleryDemoConfiguration( + title: localizations.demoTimePickerTitle, + description: localizations.demoTimePickerDescription, + documentationUrl: '$_docsBaseUrl/material/showTimePicker.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.PickerDemo(type: PickerDemoType.time), + ), + ), + GalleryDemoConfiguration( + title: localizations.demoDateRangePickerTitle, + description: localizations.demoDateRangePickerDescription, + documentationUrl: '$_docsBaseUrl/material/showDateRangePicker.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.PickerDemo(type: PickerDemoType.range), + ), + ), + ], + category: GalleryDemoCategory.material, + ), + GalleryDemo( + title: localizations.demoProgressIndicatorTitle, + icon: GalleryIcons.progressActivity, + slug: 'progress-indicator', + subtitle: localizations.demoProgressIndicatorSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoCircularProgressIndicatorTitle, + description: localizations.demoCircularProgressIndicatorDescription, + documentationUrl: + '$_docsBaseUrl/material/CircularProgressIndicator-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.ProgressIndicatorDemo( + type: ProgressIndicatorDemoType.circular, + ), + ), + ), + GalleryDemoConfiguration( + title: localizations.demoLinearProgressIndicatorTitle, + description: localizations.demoLinearProgressIndicatorDescription, + documentationUrl: + '$_docsBaseUrl/material/LinearProgressIndicator-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.ProgressIndicatorDemo( + type: ProgressIndicatorDemoType.linear, + ), + ), + ), + ], + category: GalleryDemoCategory.material, + ), + GalleryDemo( + title: localizations.demoSelectionControlsTitle, + icon: GalleryIcons.checkBox, + slug: 'selection-controls', + subtitle: localizations.demoSelectionControlsSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoSelectionControlsCheckboxTitle, + description: localizations.demoSelectionControlsCheckboxDescription, + documentationUrl: '$_docsBaseUrl/material/Checkbox-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.SelectionControlsDemo( + type: SelectionControlsDemoType.checkbox, + ), + ), + ), + GalleryDemoConfiguration( + title: localizations.demoSelectionControlsRadioTitle, + description: localizations.demoSelectionControlsRadioDescription, + documentationUrl: '$_docsBaseUrl/material/Radio-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.SelectionControlsDemo( + type: SelectionControlsDemoType.radio, + ), + ), + ), + GalleryDemoConfiguration( + title: localizations.demoSelectionControlsSwitchTitle, + description: localizations.demoSelectionControlsSwitchDescription, + documentationUrl: '$_docsBaseUrl/material/Switch-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.SelectionControlsDemo( + type: SelectionControlsDemoType.switches, + ), + ), + ), + ], + category: GalleryDemoCategory.material, + ), + GalleryDemo( + title: localizations.demoSlidersTitle, + icon: GalleryIcons.sliders, + slug: 'sliders', + subtitle: localizations.demoSlidersSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoSlidersTitle, + description: localizations.demoSlidersDescription, + documentationUrl: '$_docsBaseUrl/material/Slider-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.SlidersDemo(type: SlidersDemoType.sliders), + ), + ), + GalleryDemoConfiguration( + title: localizations.demoRangeSlidersTitle, + description: localizations.demoRangeSlidersDescription, + documentationUrl: '$_docsBaseUrl/material/RangeSlider-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.SlidersDemo( + type: SlidersDemoType.rangeSliders), + ), + ), + GalleryDemoConfiguration( + title: localizations.demoCustomSlidersTitle, + description: localizations.demoCustomSlidersDescription, + documentationUrl: '$_docsBaseUrl/material/SliderTheme-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.SlidersDemo( + type: SlidersDemoType.customSliders), + ), + ), + ], + category: GalleryDemoCategory.material, + ), + GalleryDemo( + title: localizations.demoSnackbarsTitle, + icon: GalleryIcons.snackbar, + slug: 'snackbars', + subtitle: localizations.demoSnackbarsSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoSnackbarsTitle, + description: localizations.demoSnackbarsDescription, + documentationUrl: '$_docsBaseUrl/material/SnackBar-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.SnackbarsDemo(), + ), + ), + ], + category: GalleryDemoCategory.material, + ), + GalleryDemo( + title: localizations.demoTabsTitle, + icon: GalleryIcons.tabs, + slug: 'tabs', + subtitle: localizations.demoTabsSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoTabsScrollingTitle, + description: localizations.demoTabsDescription, + documentationUrl: '$_docsBaseUrl/material/TabBar-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.TabsDemo(type: TabsDemoType.scrollable), + ), + ), + GalleryDemoConfiguration( + title: localizations.demoTabsNonScrollingTitle, + description: localizations.demoTabsDescription, + documentationUrl: '$_docsBaseUrl/material/TabBar-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.TabsDemo(type: TabsDemoType.nonScrollable), + ), + ), + ], + category: GalleryDemoCategory.material, + ), + GalleryDemo( + title: localizations.demoTextFieldTitle, + icon: GalleryIcons.textFieldsAlt, + slug: 'text-field', + subtitle: localizations.demoTextFieldSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoTextFieldTitle, + description: localizations.demoTextFieldDescription, + documentationUrl: '$_docsBaseUrl/material/TextField-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.TextFieldDemo(), + ), + ), + ], + category: GalleryDemoCategory.material, + ), + GalleryDemo( + title: localizations.demoTooltipTitle, + icon: GalleryIcons.tooltip, + slug: 'tooltip', + subtitle: localizations.demoTooltipSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoTooltipTitle, + description: localizations.demoTooltipDescription, + documentationUrl: '$_docsBaseUrl/material/Tooltip-class.html', + buildRoute: (BuildContext context) => DeferredWidget( + materialDemosLibrary, + () => material_demos.TooltipDemo(), + ), + ), + ], + category: GalleryDemoCategory.material, + ), + ]; + } + + static List cupertinoDemos(GalleryLocalizations localizations) { + final LibraryLoader cupertinoLoader = cupertino_demos.loadLibrary; + return [ + GalleryDemo( + title: localizations.demoCupertinoActivityIndicatorTitle, + icon: GalleryIcons.cupertinoProgress, + slug: 'cupertino-activity-indicator', + subtitle: localizations.demoCupertinoActivityIndicatorSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoCupertinoActivityIndicatorTitle, + description: + localizations.demoCupertinoActivityIndicatorDescription, + documentationUrl: + '$_docsBaseUrl/cupertino/CupertinoActivityIndicator-class.html', + buildRoute: (_) => DeferredWidget( + cupertinoLoader, + () => cupertino_demos.CupertinoProgressIndicatorDemo(), + ), + ), + ], + category: GalleryDemoCategory.cupertino, + ), + GalleryDemo( + title: localizations.demoCupertinoAlertsTitle, + icon: GalleryIcons.dialogs, + slug: 'cupertino-alerts', + subtitle: localizations.demoCupertinoAlertsSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoCupertinoAlertTitle, + description: localizations.demoCupertinoAlertDescription, + documentationUrl: + '$_docsBaseUrl/cupertino/CupertinoAlertDialog-class.html', + buildRoute: (_) => DeferredWidget( + cupertinoLoader, + () => cupertino_demos.CupertinoAlertDemo( + type: AlertDemoType.alert)), + ), + GalleryDemoConfiguration( + title: localizations.demoCupertinoAlertWithTitleTitle, + description: localizations.demoCupertinoAlertDescription, + documentationUrl: + '$_docsBaseUrl/cupertino/CupertinoAlertDialog-class.html', + buildRoute: (_) => DeferredWidget( + cupertinoLoader, + () => cupertino_demos.CupertinoAlertDemo( + type: AlertDemoType.alertTitle)), + ), + GalleryDemoConfiguration( + title: localizations.demoCupertinoAlertButtonsTitle, + description: localizations.demoCupertinoAlertDescription, + documentationUrl: + '$_docsBaseUrl/cupertino/CupertinoAlertDialog-class.html', + buildRoute: (_) => DeferredWidget( + cupertinoLoader, + () => cupertino_demos.CupertinoAlertDemo( + type: AlertDemoType.alertButtons)), + ), + GalleryDemoConfiguration( + title: localizations.demoCupertinoAlertButtonsOnlyTitle, + description: localizations.demoCupertinoAlertDescription, + documentationUrl: + '$_docsBaseUrl/cupertino/CupertinoAlertDialog-class.html', + buildRoute: (_) => DeferredWidget( + cupertinoLoader, + () => cupertino_demos.CupertinoAlertDemo( + type: AlertDemoType.alertButtonsOnly)), + ), + GalleryDemoConfiguration( + title: localizations.demoCupertinoActionSheetTitle, + description: localizations.demoCupertinoActionSheetDescription, + documentationUrl: + '$_docsBaseUrl/cupertino/CupertinoActionSheet-class.html', + buildRoute: (_) => DeferredWidget( + cupertinoLoader, + () => cupertino_demos.CupertinoAlertDemo( + type: AlertDemoType.actionSheet)), + ), + ], + category: GalleryDemoCategory.cupertino, + ), + GalleryDemo( + title: localizations.demoCupertinoButtonsTitle, + icon: GalleryIcons.genericButtons, + slug: 'cupertino-buttons', + subtitle: localizations.demoCupertinoButtonsSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoCupertinoButtonsTitle, + description: localizations.demoCupertinoButtonsDescription, + documentationUrl: + '$_docsBaseUrl/cupertino/CupertinoButton-class.html', + buildRoute: (_) => DeferredWidget( + cupertinoLoader, + () => cupertino_demos.CupertinoButtonDemo(), + ), + ), + ], + category: GalleryDemoCategory.cupertino, + ), + GalleryDemo( + title: localizations.demoCupertinoContextMenuTitle, + icon: GalleryIcons.moreVert, + slug: 'cupertino-context-menu', + subtitle: localizations.demoCupertinoContextMenuSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoCupertinoContextMenuTitle, + description: localizations.demoCupertinoContextMenuDescription, + documentationUrl: + '$_docsBaseUrl/cupertino/CupertinoContextMenu-class.html', + buildRoute: (_) => DeferredWidget( + cupertinoLoader, + () => cupertino_demos.CupertinoContextMenuDemo(), + ), + ), + ], + category: GalleryDemoCategory.cupertino, + ), + GalleryDemo( + title: localizations.demoCupertinoNavigationBarTitle, + icon: GalleryIcons.bottomSheetPersistent, + slug: 'cupertino-navigation-bar', + subtitle: localizations.demoCupertinoNavigationBarSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoCupertinoNavigationBarTitle, + description: localizations.demoCupertinoNavigationBarDescription, + documentationUrl: + '$_docsBaseUrl/cupertino/CupertinoNavigationBar-class.html', + buildRoute: (_) => DeferredWidget( + cupertinoLoader, + () => cupertino_demos.CupertinoNavigationBarDemo(), + ), + ), + ], + category: GalleryDemoCategory.cupertino, + ), + GalleryDemo( + title: localizations.demoCupertinoPickerTitle, + icon: GalleryIcons.listAlt, + slug: 'cupertino-picker', + subtitle: localizations.demoCupertinoPickerSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoCupertinoPickerTitle, + description: localizations.demoCupertinoPickerDescription, + documentationUrl: + '$_docsBaseUrl/cupertino/CupertinoDatePicker-class.html', + buildRoute: (_) => DeferredWidget( + cupertinoLoader, + // ignore: prefer_const_constructors + () => cupertino_demos.CupertinoPickerDemo()), + ), + ], + category: GalleryDemoCategory.cupertino, + ), + GalleryDemo( + title: localizations.demoCupertinoScrollbarTitle, + icon: GalleryIcons.listAlt, + slug: 'cupertino-scrollbar', + subtitle: localizations.demoCupertinoScrollbarSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoCupertinoScrollbarTitle, + description: localizations.demoCupertinoScrollbarDescription, + documentationUrl: + '$_docsBaseUrl/cupertino/CupertinoScrollbar-class.html', + buildRoute: (_) => DeferredWidget( + cupertinoLoader, + // ignore: prefer_const_constructors + () => cupertino_demos.CupertinoScrollbarDemo()), + ), + ], + category: GalleryDemoCategory.cupertino, + ), + GalleryDemo( + title: localizations.demoCupertinoSegmentedControlTitle, + icon: GalleryIcons.tabs, + slug: 'cupertino-segmented-control', + subtitle: localizations.demoCupertinoSegmentedControlSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoCupertinoSegmentedControlTitle, + description: localizations.demoCupertinoSegmentedControlDescription, + documentationUrl: + '$_docsBaseUrl/cupertino/CupertinoSegmentedControl-class.html', + buildRoute: (_) => DeferredWidget( + cupertinoLoader, + () => cupertino_demos.CupertinoSegmentedControlDemo(), + ), + ), + ], + category: GalleryDemoCategory.cupertino, + ), + GalleryDemo( + title: localizations.demoCupertinoSliderTitle, + icon: GalleryIcons.sliders, + slug: 'cupertino-slider', + subtitle: localizations.demoCupertinoSliderSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoCupertinoSliderTitle, + description: localizations.demoCupertinoSliderDescription, + documentationUrl: + '$_docsBaseUrl/cupertino/CupertinoSlider-class.html', + buildRoute: (_) => DeferredWidget( + cupertinoLoader, + () => cupertino_demos.CupertinoSliderDemo(), + ), + ), + ], + category: GalleryDemoCategory.cupertino, + ), + GalleryDemo( + title: localizations.demoSelectionControlsSwitchTitle, + icon: GalleryIcons.cupertinoSwitch, + slug: 'cupertino-switch', + subtitle: localizations.demoCupertinoSwitchSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoSelectionControlsSwitchTitle, + description: localizations.demoCupertinoSwitchDescription, + documentationUrl: + '$_docsBaseUrl/cupertino/CupertinoSwitch-class.html', + buildRoute: (_) => DeferredWidget( + cupertinoLoader, + () => cupertino_demos.CupertinoSwitchDemo(), + ), + ), + ], + category: GalleryDemoCategory.cupertino, + ), + GalleryDemo( + title: localizations.demoCupertinoTabBarTitle, + icon: GalleryIcons.bottomNavigation, + slug: 'cupertino-tab-bar', + subtitle: localizations.demoCupertinoTabBarSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoCupertinoTabBarTitle, + description: localizations.demoCupertinoTabBarDescription, + documentationUrl: + '$_docsBaseUrl/cupertino/CupertinoTabBar-class.html', + buildRoute: (_) => DeferredWidget( + cupertinoLoader, + () => cupertino_demos.CupertinoTabBarDemo(), + ), + ), + ], + category: GalleryDemoCategory.cupertino, + ), + GalleryDemo( + title: localizations.demoCupertinoTextFieldTitle, + icon: GalleryIcons.textFieldsAlt, + slug: 'cupertino-text-field', + subtitle: localizations.demoCupertinoTextFieldSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoCupertinoTextFieldTitle, + description: localizations.demoCupertinoTextFieldDescription, + documentationUrl: + '$_docsBaseUrl/cupertino/CupertinoTextField-class.html', + buildRoute: (_) => DeferredWidget( + cupertinoLoader, + () => cupertino_demos.CupertinoTextFieldDemo(), + ), + ), + ], + category: GalleryDemoCategory.cupertino, + ), + GalleryDemo( + title: localizations.demoCupertinoSearchTextFieldTitle, + icon: GalleryIcons.search, + slug: 'cupertino-search-text-field', + subtitle: localizations.demoCupertinoSearchTextFieldSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoCupertinoSearchTextFieldTitle, + description: localizations.demoCupertinoSearchTextFieldDescription, + documentationUrl: + '$_docsBaseUrl/cupertino/CupertinoSearchTextField-class.html', + buildRoute: (_) => DeferredWidget( + cupertinoLoader, + () => cupertino_demos.CupertinoSearchTextFieldDemo(), + ), + ), + ], + category: GalleryDemoCategory.cupertino, + ), + ]; + } + + static List otherDemos(GalleryLocalizations localizations) { + return [ + GalleryDemo( + title: localizations.demoTwoPaneTitle, + icon: GalleryIcons.bottomSheetPersistent, + slug: 'two-pane', + subtitle: localizations.demoTwoPaneSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoTwoPaneFoldableLabel, + description: localizations.demoTwoPaneFoldableDescription, + documentationUrl: + 'https://pub.dev/documentation/dual_screen/latest/dual_screen/TwoPane-class.html', + buildRoute: (_) => DeferredWidget( + twopane_demo.loadLibrary, + () => twopane_demo.TwoPaneDemo( + type: twopane_demo.TwoPaneDemoType.foldable, + restorationId: 'two_pane_foldable', + ), + ), + ), + GalleryDemoConfiguration( + title: localizations.demoTwoPaneTabletLabel, + description: localizations.demoTwoPaneTabletDescription, + documentationUrl: + 'https://pub.dev/documentation/dual_screen/latest/dual_screen/TwoPane-class.html', + buildRoute: (_) => DeferredWidget( + twopane_demo.loadLibrary, + () => twopane_demo.TwoPaneDemo( + type: twopane_demo.TwoPaneDemoType.tablet, + restorationId: 'two_pane_tablet', + ), + ), + ), + GalleryDemoConfiguration( + title: localizations.demoTwoPaneSmallScreenLabel, + description: localizations.demoTwoPaneSmallScreenDescription, + documentationUrl: + 'https://pub.dev/documentation/dual_screen/latest/dual_screen/TwoPane-class.html', + buildRoute: (_) => DeferredWidget( + twopane_demo.loadLibrary, + () => twopane_demo.TwoPaneDemo( + type: twopane_demo.TwoPaneDemoType.smallScreen, + restorationId: 'two_pane_single', + ), + ), + ), + ], + category: GalleryDemoCategory.other, + ), + GalleryDemo( + title: localizations.demoMotionTitle, + icon: GalleryIcons.animation, + slug: 'motion', + subtitle: localizations.demoMotionSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoContainerTransformTitle, + description: localizations.demoContainerTransformDescription, + documentationUrl: '$_docsAnimationsUrl/OpenContainer-class.html', + buildRoute: (_) => DeferredWidget( + motion_demo_container.loadLibrary, + () => motion_demo_container.OpenContainerTransformDemo(), + ), + ), + GalleryDemoConfiguration( + title: localizations.demoSharedXAxisTitle, + description: localizations.demoSharedAxisDescription, + documentationUrl: + '$_docsAnimationsUrl/SharedAxisTransition-class.html', + buildRoute: (_) => const SharedXAxisTransitionDemo(), + ), + GalleryDemoConfiguration( + title: localizations.demoSharedYAxisTitle, + description: localizations.demoSharedAxisDescription, + documentationUrl: + '$_docsAnimationsUrl/SharedAxisTransition-class.html', + buildRoute: (_) => const SharedYAxisTransitionDemo(), + ), + GalleryDemoConfiguration( + title: localizations.demoSharedZAxisTitle, + description: localizations.demoSharedAxisDescription, + documentationUrl: + '$_docsAnimationsUrl/SharedAxisTransition-class.html', + buildRoute: (_) => const SharedZAxisTransitionDemo(), + ), + GalleryDemoConfiguration( + title: localizations.demoFadeThroughTitle, + description: localizations.demoFadeThroughDescription, + documentationUrl: + '$_docsAnimationsUrl/FadeThroughTransition-class.html', + buildRoute: (_) => const FadeThroughTransitionDemo(), + ), + GalleryDemoConfiguration( + title: localizations.demoFadeScaleTitle, + description: localizations.demoFadeScaleDescription, + documentationUrl: + '$_docsAnimationsUrl/FadeScaleTransition-class.html', + buildRoute: (_) => const FadeScaleTransitionDemo(), + ), + ], + category: GalleryDemoCategory.other, + ), + GalleryDemo( + title: localizations.demoColorsTitle, + icon: GalleryIcons.colors, + slug: 'colors', + subtitle: localizations.demoColorsSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoColorsTitle, + description: localizations.demoColorsDescription, + documentationUrl: '$_docsBaseUrl/material/MaterialColor-class.html', + buildRoute: (_) => DeferredWidget( + colors_demo.loadLibrary, + () => colors_demo.ColorsDemo(), + ), + ), + ], + category: GalleryDemoCategory.other, + ), + GalleryDemo( + title: localizations.demoTypographyTitle, + icon: GalleryIcons.customTypography, + slug: 'typography', + subtitle: localizations.demoTypographySubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoTypographyTitle, + description: localizations.demoTypographyDescription, + documentationUrl: '$_docsBaseUrl/material/TextTheme-class.html', + buildRoute: (_) => DeferredWidget( + typography.loadLibrary, + () => typography.TypographyDemo(), + ), + ), + ], + category: GalleryDemoCategory.other, + ), + GalleryDemo( + title: localizations.demo2dTransformationsTitle, + icon: GalleryIcons.gridOn, + slug: '2d-transformations', + subtitle: localizations.demo2dTransformationsSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demo2dTransformationsTitle, + description: localizations.demo2dTransformationsDescription, + documentationUrl: + '$_docsBaseUrl/widgets/GestureDetector-class.html', + buildRoute: (_) => DeferredWidget( + transformations_demo.loadLibrary, + () => transformations_demo.TransformationsDemo(), + ), + ), + ], + category: GalleryDemoCategory.other, + ), + ]; + } +} diff --git a/dev/integration_tests/new_gallery/lib/data/gallery_options.dart b/dev/integration_tests/new_gallery/lib/data/gallery_options.dart new file mode 100644 index 0000000000..b896e4dcc0 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/data/gallery_options.dart @@ -0,0 +1,282 @@ +// 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 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart' show timeDilation; +import 'package:flutter/services.dart' show SystemUiOverlayStyle; +import '../constants.dart'; + +enum CustomTextDirection { + localeBased, + ltr, + rtl, +} + +// See http://en.wikipedia.org/wiki/Right-to-left +const List rtlLanguages = [ + 'ar', // Arabic + 'fa', // Farsi + 'he', // Hebrew + 'ps', // Pashto + 'ur', // Urdu +]; + +// Fake locale to represent the system Locale option. +const Locale systemLocaleOption = Locale('system'); + +Locale? _deviceLocale; + +Locale? get deviceLocale => _deviceLocale; + +set deviceLocale(Locale? locale) { + _deviceLocale ??= locale; +} + +@immutable +class GalleryOptions { + const GalleryOptions({ + required this.themeMode, + required double? textScaleFactor, + required this.customTextDirection, + required Locale? locale, + required this.timeDilation, + required this.platform, + required this.isTestMode, + }) : _textScaleFactor = textScaleFactor ?? 1.0, + _locale = locale; + + final ThemeMode themeMode; + final double _textScaleFactor; + final CustomTextDirection customTextDirection; + final Locale? _locale; + final double timeDilation; + final TargetPlatform? platform; + final bool isTestMode; // True for integration tests. + + // We use a sentinel value to indicate the system text scale option. By + // default, return the actual text scale factor, otherwise return the + // sentinel value. + double textScaleFactor(BuildContext context, {bool useSentinel = false}) { + if (_textScaleFactor == systemTextScaleFactorOption) { + return useSentinel + ? systemTextScaleFactorOption + // ignore: deprecated_member_use + : MediaQuery.of(context).textScaleFactor; + } else { + return _textScaleFactor; + } + } + + Locale? get locale => _locale ?? deviceLocale; + + /// Returns a text direction based on the [CustomTextDirection] setting. + /// If it is based on locale and the locale cannot be determined, returns + /// null. + TextDirection? resolvedTextDirection() { + switch (customTextDirection) { + case CustomTextDirection.localeBased: + final String? language = locale?.languageCode.toLowerCase(); + if (language == null) { + return null; + } + return rtlLanguages.contains(language) + ? TextDirection.rtl + : TextDirection.ltr; + case CustomTextDirection.rtl: + return TextDirection.rtl; + case CustomTextDirection.ltr: + return TextDirection.ltr; + } + } + + /// Returns a [SystemUiOverlayStyle] based on the [ThemeMode] setting. + /// In other words, if the theme is dark, returns light; if the theme is + /// light, returns dark. + SystemUiOverlayStyle resolvedSystemUiOverlayStyle() { + Brightness brightness; + switch (themeMode) { + case ThemeMode.light: + brightness = Brightness.light; + case ThemeMode.dark: + brightness = Brightness.dark; + case ThemeMode.system: + brightness = + WidgetsBinding.instance.platformDispatcher.platformBrightness; + } + + final SystemUiOverlayStyle overlayStyle = brightness == Brightness.dark + ? SystemUiOverlayStyle.light + : SystemUiOverlayStyle.dark; + + return overlayStyle; + } + + GalleryOptions copyWith({ + ThemeMode? themeMode, + double? textScaleFactor, + CustomTextDirection? customTextDirection, + Locale? locale, + double? timeDilation, + TargetPlatform? platform, + bool? isTestMode, + }) { + return GalleryOptions( + themeMode: themeMode ?? this.themeMode, + textScaleFactor: textScaleFactor ?? _textScaleFactor, + customTextDirection: customTextDirection ?? this.customTextDirection, + locale: locale ?? this.locale, + timeDilation: timeDilation ?? this.timeDilation, + platform: platform ?? this.platform, + isTestMode: isTestMode ?? this.isTestMode, + ); + } + + @override + bool operator ==(Object other) => + other is GalleryOptions && + themeMode == other.themeMode && + _textScaleFactor == other._textScaleFactor && + customTextDirection == other.customTextDirection && + locale == other.locale && + timeDilation == other.timeDilation && + platform == other.platform && + isTestMode == other.isTestMode; + + @override + int get hashCode => Object.hash( + themeMode, + _textScaleFactor, + customTextDirection, + locale, + timeDilation, + platform, + isTestMode, + ); + + static GalleryOptions of(BuildContext context) { + final _ModelBindingScope scope = + context.dependOnInheritedWidgetOfExactType<_ModelBindingScope>()!; + return scope.modelBindingState.currentModel; + } + + static void update(BuildContext context, GalleryOptions newModel) { + final _ModelBindingScope scope = + context.dependOnInheritedWidgetOfExactType<_ModelBindingScope>()!; + scope.modelBindingState.updateModel(newModel); + } +} + +// Applies text GalleryOptions to a widget +class ApplyTextOptions extends StatelessWidget { + const ApplyTextOptions({ + super.key, + required this.child, + }); + + final Widget child; + + @override + Widget build(BuildContext context) { + final GalleryOptions options = GalleryOptions.of(context); + final TextDirection? textDirection = options.resolvedTextDirection(); + final double textScaleFactor = options.textScaleFactor(context); + + final Widget widget = MediaQuery( + data: MediaQuery.of(context).copyWith( + // ignore: deprecated_member_use + textScaleFactor: textScaleFactor, + ), + child: child, + ); + return textDirection == null + ? widget + : Directionality( + textDirection: textDirection, + child: widget, + ); + } +} + +// Everything below is boilerplate except code relating to time dilation. +// See https://medium.com/flutter/managing-flutter-application-state-with-inheritedwidgets-1140452befe1 + +class _ModelBindingScope extends InheritedWidget { + const _ModelBindingScope({ + required this.modelBindingState, + required super.child, + }); + + final _ModelBindingState modelBindingState; + + @override + bool updateShouldNotify(_ModelBindingScope oldWidget) => true; +} + +class ModelBinding extends StatefulWidget { + const ModelBinding({ + super.key, + required this.initialModel, + required this.child, + }); + + final GalleryOptions initialModel; + final Widget child; + + @override + State createState() => _ModelBindingState(); +} + +class _ModelBindingState extends State { + late GalleryOptions currentModel; + Timer? _timeDilationTimer; + + @override + void initState() { + super.initState(); + currentModel = widget.initialModel; + } + + @override + void dispose() { + _timeDilationTimer?.cancel(); + _timeDilationTimer = null; + super.dispose(); + } + + void handleTimeDilation(GalleryOptions newModel) { + if (currentModel.timeDilation != newModel.timeDilation) { + _timeDilationTimer?.cancel(); + _timeDilationTimer = null; + if (newModel.timeDilation > 1) { + // We delay the time dilation change long enough that the user can see + // that UI has started reacting and then we slam on the brakes so that + // they see that the time is in fact now dilated. + _timeDilationTimer = Timer(const Duration(milliseconds: 150), () { + timeDilation = newModel.timeDilation; + }); + } else { + timeDilation = newModel.timeDilation; + } + } + } + + void updateModel(GalleryOptions newModel) { + if (newModel != currentModel) { + handleTimeDilation(newModel); + setState(() { + currentModel = newModel; + }); + } + } + + @override + Widget build(BuildContext context) { + return _ModelBindingScope( + modelBindingState: this, + child: widget.child, + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/data/icons.dart b/dev/integration_tests/new_gallery/lib/data/icons.dart new file mode 100644 index 0000000000..970501f496 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/data/icons.dart @@ -0,0 +1,174 @@ +// 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:flutter/material.dart'; + +class GalleryIcons { + GalleryIcons._(); + + static const IconData tooltip = IconData( + 0xe900, + fontFamily: 'GalleryIcons', + ); + static const IconData textFieldsAlt = IconData( + 0xe901, + fontFamily: 'GalleryIcons', + ); + static const IconData tabs = IconData( + 0xe902, + fontFamily: 'GalleryIcons', + ); + static const IconData switches = IconData( + 0xe903, + fontFamily: 'GalleryIcons', + ); + static const IconData sliders = IconData( + 0xe904, + fontFamily: 'GalleryIcons', + ); + static const IconData shrine = IconData( + 0xe905, + fontFamily: 'GalleryIcons', + ); + static const IconData sentimentVerySatisfied = IconData( + 0xe906, + fontFamily: 'GalleryIcons', + ); + static const IconData refresh = IconData( + 0xe907, + fontFamily: 'GalleryIcons', + ); + static const IconData progressActivity = IconData( + 0xe908, + fontFamily: 'GalleryIcons', + ); + static const IconData phoneIphone = IconData( + 0xe909, + fontFamily: 'GalleryIcons', + ); + static const IconData pageControl = IconData( + 0xe90a, + fontFamily: 'GalleryIcons', + ); + static const IconData moreVert = IconData( + 0xe90b, + fontFamily: 'GalleryIcons', + ); + static const IconData menu = IconData( + 0xe90c, + fontFamily: 'GalleryIcons', + ); + static const IconData listAlt = IconData( + 0xe90d, + fontFamily: 'GalleryIcons', + ); + static const IconData gridOn = IconData( + 0xe90e, + fontFamily: 'GalleryIcons', + ); + static const IconData expandAll = IconData( + 0xe90f, + fontFamily: 'GalleryIcons', + ); + static const IconData event = IconData( + 0xe910, + fontFamily: 'GalleryIcons', + ); + static const IconData driveVideo = IconData( + 0xe911, + fontFamily: 'GalleryIcons', + ); + static const IconData dialogs = IconData( + 0xe912, + fontFamily: 'GalleryIcons', + ); + static const IconData dataTable = IconData( + 0xe913, + fontFamily: 'GalleryIcons', + ); + static const IconData customTypography = IconData( + 0xe914, + fontFamily: 'GalleryIcons', + ); + static const IconData colors = IconData( + 0xe915, + fontFamily: 'GalleryIcons', + ); + static const IconData chips = IconData( + 0xe916, + fontFamily: 'GalleryIcons', + ); + static const IconData checkBox = IconData( + 0xe917, + fontFamily: 'GalleryIcons', + ); + static const IconData cards = IconData( + 0xe918, + fontFamily: 'GalleryIcons', + ); + static const IconData buttons = IconData( + 0xe919, + fontFamily: 'GalleryIcons', + ); + static const IconData bottomSheets = IconData( + 0xe91a, + fontFamily: 'GalleryIcons', + ); + static const IconData bottomNavigation = IconData( + 0xe91b, + fontFamily: 'GalleryIcons', + ); + static const IconData animation = IconData( + 0xe91c, + fontFamily: 'GalleryIcons', + ); + static const IconData accountBox = IconData( + 0xe91d, + fontFamily: 'GalleryIcons', + ); + static const IconData snackbar = IconData( + 0xe91e, + fontFamily: 'GalleryIcons', + ); + static const IconData categoryMdc = IconData( + 0xe91f, + fontFamily: 'GalleryIcons', + ); + static const IconData cupertinoProgress = IconData( + 0xe920, + fontFamily: 'GalleryIcons', + ); + static const IconData cupertinoPullToRefresh = IconData( + 0xe921, + fontFamily: 'GalleryIcons', + ); + static const IconData cupertinoSwitch = IconData( + 0xe922, + fontFamily: 'GalleryIcons', + ); + static const IconData genericButtons = IconData( + 0xe923, + fontFamily: 'GalleryIcons', + ); + static const IconData backdrop = IconData( + 0xe924, + fontFamily: 'GalleryIcons', + ); + static const IconData bottomAppBar = IconData( + 0xe925, + fontFamily: 'GalleryIcons', + ); + static const IconData bottomSheetPersistent = IconData( + 0xe926, + fontFamily: 'GalleryIcons', + ); + static const IconData listsLeaveBehind = IconData( + 0xe927, + fontFamily: 'GalleryIcons', + ); + static const IconData navigationRail = Icons.vertical_split; + static const IconData appbar = Icons.web_asset; + static const IconData divider = Icons.credit_card; + static const IconData search = Icons.search; +} diff --git a/dev/integration_tests/new_gallery/lib/deferred_widget.dart b/dev/integration_tests/new_gallery/lib/deferred_widget.dart new file mode 100644 index 0000000000..bcbff0eaa0 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/deferred_widget.dart @@ -0,0 +1,120 @@ +// 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 'dart:async'; +import 'package:flutter/material.dart'; + +typedef LibraryLoader = Future Function(); +typedef DeferredWidgetBuilder = Widget Function(); + +/// Wraps the child inside a deferred module loader. +/// +/// The child is created and a single instance of the Widget is maintained in +/// state as long as closure to create widget stays the same. +/// +class DeferredWidget extends StatefulWidget { + DeferredWidget( + this.libraryLoader, + this.createWidget, { + super.key, + Widget? placeholder, + }) : placeholder = placeholder ?? Container(); + + final LibraryLoader libraryLoader; + final DeferredWidgetBuilder createWidget; + final Widget placeholder; + static final Map> _moduleLoaders = >{}; + static final Set _loadedModules = {}; + + static Future preload(LibraryLoader loader) { + if (!_moduleLoaders.containsKey(loader)) { + _moduleLoaders[loader] = loader().then((dynamic _) { + _loadedModules.add(loader); + }); + } + return _moduleLoaders[loader]!; + } + + @override + State createState() => _DeferredWidgetState(); +} + +class _DeferredWidgetState extends State { + _DeferredWidgetState(); + + Widget? _loadedChild; + DeferredWidgetBuilder? _loadedCreator; + + @override + void initState() { + /// If module was already loaded immediately create widget instead of + /// waiting for future or zone turn. + if (DeferredWidget._loadedModules.contains(widget.libraryLoader)) { + _onLibraryLoaded(); + } else { + DeferredWidget.preload(widget.libraryLoader) + .then((dynamic _) => _onLibraryLoaded()); + } + super.initState(); + } + + void _onLibraryLoaded() { + setState(() { + _loadedCreator = widget.createWidget; + _loadedChild = _loadedCreator!(); + }); + } + + @override + Widget build(BuildContext context) { + /// If closure to create widget changed, create new instance, otherwise + /// treat as const Widget. + if (_loadedCreator != widget.createWidget && _loadedCreator != null) { + _loadedCreator = widget.createWidget; + _loadedChild = _loadedCreator!(); + } + return _loadedChild ?? widget.placeholder; + } +} + +/// Displays a progress indicator and text description explaining that +/// the widget is a deferred component and is currently being installed. +class DeferredLoadingPlaceholder extends StatelessWidget { + const DeferredLoadingPlaceholder({ + super.key, + this.name = 'This widget', + }); + + final String name; + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + decoration: BoxDecoration( + color: Colors.grey[700], + border: Border.all( + width: 20, + color: Colors.grey[700]!, + ), + borderRadius: const BorderRadius.all(Radius.circular(10))), + width: 250, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('$name is installing.', + style: Theme.of(context).textTheme.headlineMedium), + Container(height: 10), + Text( + '$name is a deferred component which are downloaded and installed at runtime.', + style: Theme.of(context).textTheme.bodyLarge), + Container(height: 20), + const Center(child: CircularProgressIndicator()), + ], + ), + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_activity_indicator_demo.dart b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_activity_indicator_demo.dart new file mode 100644 index 0000000000..db8bdd9912 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_activity_indicator_demo.dart @@ -0,0 +1,29 @@ +// 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:flutter/cupertino.dart'; +import '../../gallery_localizations.dart'; + +// BEGIN cupertinoActivityIndicatorDemo + +class CupertinoProgressIndicatorDemo extends StatelessWidget { + const CupertinoProgressIndicatorDemo({super.key}); + + @override + Widget build(BuildContext context) { + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + automaticallyImplyLeading: false, + middle: Text( + GalleryLocalizations.of(context)!.demoCupertinoActivityIndicatorTitle, + ), + ), + child: const Center( + child: CupertinoActivityIndicator(), + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_alert_demo.dart b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_alert_demo.dart new file mode 100644 index 0000000000..96cc821c2d --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_alert_demo.dart @@ -0,0 +1,432 @@ +// 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:flutter/cupertino.dart'; + +import '../../data/gallery_options.dart'; +import '../../gallery_localizations.dart'; +import 'demo_types.dart'; + +// BEGIN cupertinoAlertDemo + +class CupertinoAlertDemo extends StatefulWidget { + const CupertinoAlertDemo({ + super.key, + required this.type, + }); + + final AlertDemoType type; + + @override + State createState() => _CupertinoAlertDemoState(); +} + +class _CupertinoAlertDemoState extends State + with RestorationMixin { + RestorableStringN lastSelectedValue = RestorableStringN(null); + late RestorableRouteFuture _alertDialogRoute; + late RestorableRouteFuture _alertWithTitleDialogRoute; + late RestorableRouteFuture _alertWithButtonsDialogRoute; + late RestorableRouteFuture _alertWithButtonsOnlyDialogRoute; + late RestorableRouteFuture _modalPopupRoute; + + @override + String get restorationId => 'cupertino_alert_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration( + lastSelectedValue, + 'last_selected_value', + ); + registerForRestoration( + _alertDialogRoute, + 'alert_demo_dialog_route', + ); + registerForRestoration( + _alertWithTitleDialogRoute, + 'alert_with_title_press_demo_dialog_route', + ); + registerForRestoration( + _alertWithButtonsDialogRoute, + 'alert_with_title_press_demo_dialog_route', + ); + registerForRestoration( + _alertWithButtonsOnlyDialogRoute, + 'alert_with_title_press_demo_dialog_route', + ); + registerForRestoration( + _modalPopupRoute, + 'modal_popup_route', + ); + } + + void _setSelectedValue(String value) { + setState(() { + lastSelectedValue.value = value; + }); + } + + @override + void initState() { + super.initState(); + _alertDialogRoute = RestorableRouteFuture( + onPresent: (NavigatorState navigator, Object? arguments) { + return navigator.restorablePush(_alertDemoDialog); + }, + onComplete: _setSelectedValue, + ); + _alertWithTitleDialogRoute = RestorableRouteFuture( + onPresent: (NavigatorState navigator, Object? arguments) { + return navigator.restorablePush(_alertWithTitleDialog); + }, + onComplete: _setSelectedValue, + ); + _alertWithButtonsDialogRoute = RestorableRouteFuture( + onPresent: (NavigatorState navigator, Object? arguments) { + return navigator.restorablePush(_alertWithButtonsDialog); + }, + onComplete: _setSelectedValue, + ); + _alertWithButtonsOnlyDialogRoute = RestorableRouteFuture( + onPresent: (NavigatorState navigator, Object? arguments) { + return navigator.restorablePush(_alertWithButtonsOnlyDialog); + }, + onComplete: _setSelectedValue, + ); + _modalPopupRoute = RestorableRouteFuture( + onPresent: (NavigatorState navigator, Object? arguments) { + return navigator.restorablePush(_modalRoute); + }, + onComplete: _setSelectedValue, + ); + } + + String _title(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + switch (widget.type) { + case AlertDemoType.alert: + return localizations.demoCupertinoAlertTitle; + case AlertDemoType.alertTitle: + return localizations.demoCupertinoAlertWithTitleTitle; + case AlertDemoType.alertButtons: + return localizations.demoCupertinoAlertButtonsTitle; + case AlertDemoType.alertButtonsOnly: + return localizations.demoCupertinoAlertButtonsOnlyTitle; + case AlertDemoType.actionSheet: + return localizations.demoCupertinoActionSheetTitle; + } + } + + static Route _alertDemoDialog( + BuildContext context, + Object? arguments, + ) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return CupertinoDialogRoute( + context: context, + builder: (BuildContext context) => ApplyTextOptions( + child: CupertinoAlertDialog( + title: Text(localizations.dialogDiscardTitle), + actions: [ + CupertinoDialogAction( + isDestructiveAction: true, + onPressed: () { + Navigator.of( + context, + ).pop(localizations.cupertinoAlertDiscard); + }, + child: Text( + localizations.cupertinoAlertDiscard, + ), + ), + CupertinoDialogAction( + isDefaultAction: true, + onPressed: () => Navigator.of( + context, + ).pop( + localizations.cupertinoAlertCancel, + ), + child: Text( + localizations.cupertinoAlertCancel, + ), + ), + ], + ), + ), + ); + } + + static Route _alertWithTitleDialog( + BuildContext context, + Object? arguments, + ) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return CupertinoDialogRoute( + context: context, + builder: (BuildContext context) => ApplyTextOptions( + child: CupertinoAlertDialog( + title: Text( + localizations.cupertinoAlertLocationTitle, + ), + content: Text( + localizations.cupertinoAlertLocationDescription, + ), + actions: [ + CupertinoDialogAction( + onPressed: () => Navigator.of( + context, + ).pop( + localizations.cupertinoAlertDontAllow, + ), + child: Text( + localizations.cupertinoAlertDontAllow, + ), + ), + CupertinoDialogAction( + onPressed: () => Navigator.of( + context, + ).pop( + localizations.cupertinoAlertAllow, + ), + child: Text( + localizations.cupertinoAlertAllow, + ), + ), + ], + ), + ), + ); + } + + static Route _alertWithButtonsDialog( + BuildContext context, + Object? arguments, + ) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return CupertinoDialogRoute( + context: context, + builder: (BuildContext context) => ApplyTextOptions( + child: CupertinoDessertDialog( + title: Text( + localizations.cupertinoAlertFavoriteDessert, + ), + content: Text( + localizations.cupertinoAlertDessertDescription, + ), + ), + ), + ); + } + + static Route _alertWithButtonsOnlyDialog( + BuildContext context, + Object? arguments, + ) { + return CupertinoDialogRoute( + context: context, + builder: (BuildContext context) => const ApplyTextOptions( + child: CupertinoDessertDialog(), + ), + ); + } + + static Route _modalRoute( + BuildContext context, + Object? arguments, + ) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return CupertinoModalPopupRoute( + builder: (BuildContext context) => ApplyTextOptions( + child: CupertinoActionSheet( + title: Text( + localizations.cupertinoAlertFavoriteDessert, + ), + message: Text( + localizations.cupertinoAlertDessertDescription, + ), + actions: [ + CupertinoActionSheetAction( + onPressed: () => Navigator.of( + context, + ).pop( + localizations.cupertinoAlertCheesecake, + ), + child: Text( + localizations.cupertinoAlertCheesecake, + ), + ), + CupertinoActionSheetAction( + onPressed: () => Navigator.of( + context, + ).pop( + localizations.cupertinoAlertTiramisu, + ), + child: Text( + localizations.cupertinoAlertTiramisu, + ), + ), + CupertinoActionSheetAction( + onPressed: () => Navigator.of( + context, + ).pop( + localizations.cupertinoAlertApplePie, + ), + child: Text( + localizations.cupertinoAlertApplePie, + ), + ), + ], + cancelButton: CupertinoActionSheetAction( + isDefaultAction: true, + onPressed: () => Navigator.of( + context, + ).pop( + localizations.cupertinoAlertCancel, + ), + child: Text( + localizations.cupertinoAlertCancel, + ), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + automaticallyImplyLeading: false, + middle: Text(_title(context)), + ), + child: Builder( + builder: (BuildContext context) { + return Column( + children: [ + Expanded( + child: Center( + child: CupertinoButton.filled( + onPressed: () { + switch (widget.type) { + case AlertDemoType.alert: + _alertDialogRoute.present(); + case AlertDemoType.alertTitle: + _alertWithTitleDialogRoute.present(); + case AlertDemoType.alertButtons: + _alertWithButtonsDialogRoute.present(); + case AlertDemoType.alertButtonsOnly: + _alertWithButtonsOnlyDialogRoute.present(); + case AlertDemoType.actionSheet: + _modalPopupRoute.present(); + } + }, + child: Text( + GalleryLocalizations.of(context)!.cupertinoShowAlert, + ), + ), + ), + ), + if (lastSelectedValue.value != null) + Padding( + padding: const EdgeInsets.all(16), + child: Text( + GalleryLocalizations.of(context)! + .dialogSelectedOption(lastSelectedValue.value!), + style: CupertinoTheme.of(context).textTheme.textStyle, + textAlign: TextAlign.center, + ), + ), + ], + ); + }, + ), + ); + } +} + +class CupertinoDessertDialog extends StatelessWidget { + const CupertinoDessertDialog({ + super.key, + this.title, + this.content, + }); + + final Widget? title; + final Widget? content; + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return CupertinoAlertDialog( + title: title, + content: content, + actions: [ + CupertinoDialogAction( + onPressed: () { + Navigator.of( + context, + ).pop( + localizations.cupertinoAlertCheesecake, + ); + }, + child: Text( + localizations.cupertinoAlertCheesecake, + ), + ), + CupertinoDialogAction( + onPressed: () { + Navigator.of( + context, + ).pop( + localizations.cupertinoAlertTiramisu, + ); + }, + child: Text( + localizations.cupertinoAlertTiramisu, + ), + ), + CupertinoDialogAction( + onPressed: () { + Navigator.of( + context, + ).pop( + localizations.cupertinoAlertApplePie, + ); + }, + child: Text( + localizations.cupertinoAlertApplePie, + ), + ), + CupertinoDialogAction( + onPressed: () { + Navigator.of( + context, + ).pop( + localizations.cupertinoAlertChocolateBrownie, + ); + }, + child: Text( + localizations.cupertinoAlertChocolateBrownie, + ), + ), + CupertinoDialogAction( + isDestructiveAction: true, + onPressed: () { + Navigator.of( + context, + ).pop( + localizations.cupertinoAlertCancel, + ); + }, + child: Text( + localizations.cupertinoAlertCancel, + ), + ), + ], + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_button_demo.dart b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_button_demo.dart new file mode 100644 index 0000000000..971f3bc6aa --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_button_demo.dart @@ -0,0 +1,60 @@ +// 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:flutter/cupertino.dart'; +import '../../gallery_localizations.dart'; + +// BEGIN cupertinoButtonDemo + +class CupertinoButtonDemo extends StatelessWidget { + const CupertinoButtonDemo({super.key}); + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + automaticallyImplyLeading: false, + middle: Text(localizations.demoCupertinoButtonsTitle), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CupertinoButton( + onPressed: () {}, + child: Text( + localizations.cupertinoButton, + ), + ), + const SizedBox(height: 16), + CupertinoButton.filled( + onPressed: () {}, + child: Text( + localizations.cupertinoButtonWithBackground, + ), + ), + const SizedBox(height: 30), + // Disabled buttons + CupertinoButton( + onPressed: null, + child: Text( + localizations.cupertinoButton, + ), + ), + const SizedBox(height: 16), + CupertinoButton.filled( + onPressed: null, + child: Text( + localizations.cupertinoButtonWithBackground, + ), + ), + ], + ), + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_context_menu_demo.dart b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_context_menu_demo.dart new file mode 100644 index 0000000000..34a70f6569 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_context_menu_demo.dart @@ -0,0 +1,71 @@ +// 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:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import '../../gallery_localizations.dart'; + +// BEGIN cupertinoContextMenuDemo + +class CupertinoContextMenuDemo extends StatelessWidget { + const CupertinoContextMenuDemo({super.key}); + + @override + Widget build(BuildContext context) { + final GalleryLocalizations galleryLocalizations = GalleryLocalizations.of(context)!; + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + automaticallyImplyLeading: false, + middle: Text( + galleryLocalizations.demoCupertinoContextMenuTitle, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: SizedBox( + width: 100, + height: 100, + child: CupertinoContextMenu( + actions: [ + CupertinoContextMenuAction( + onPressed: () { + Navigator.pop(context); + }, + child: Text( + galleryLocalizations.demoCupertinoContextMenuActionOne, + ), + ), + CupertinoContextMenuAction( + onPressed: () { + Navigator.pop(context); + }, + child: Text( + galleryLocalizations.demoCupertinoContextMenuActionTwo, + ), + ), + ], + child: const FlutterLogo(size: 250), + ), + ), + ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.all(30), + child: Text( + galleryLocalizations.demoCupertinoContextMenuActionText, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.black, + ), + ), + ), + ], + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_demos.dart b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_demos.dart new file mode 100644 index 0000000000..c365a421e1 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_demos.dart @@ -0,0 +1,17 @@ +// 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. + +export 'package:gallery/demos/cupertino/cupertino_activity_indicator_demo.dart'; +export 'package:gallery/demos/cupertino/cupertino_alert_demo.dart'; +export 'package:gallery/demos/cupertino/cupertino_button_demo.dart'; +export 'package:gallery/demos/cupertino/cupertino_context_menu_demo.dart'; +export 'package:gallery/demos/cupertino/cupertino_navigation_bar_demo.dart'; +export 'package:gallery/demos/cupertino/cupertino_picker_demo.dart'; +export 'package:gallery/demos/cupertino/cupertino_scrollbar_demo.dart'; +export 'package:gallery/demos/cupertino/cupertino_search_text_field_demo.dart'; +export 'package:gallery/demos/cupertino/cupertino_segmented_control_demo.dart'; +export 'package:gallery/demos/cupertino/cupertino_slider_demo.dart'; +export 'package:gallery/demos/cupertino/cupertino_switch_demo.dart'; +export 'package:gallery/demos/cupertino/cupertino_tab_bar_demo.dart'; +export 'package:gallery/demos/cupertino/cupertino_text_field_demo.dart'; diff --git a/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_navigation_bar_demo.dart b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_navigation_bar_demo.dart new file mode 100644 index 0000000000..073c1f5d87 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_navigation_bar_demo.dart @@ -0,0 +1,112 @@ +// 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:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import '../../gallery_localizations.dart'; + +// BEGIN cupertinoNavigationBarDemo + +class CupertinoNavigationBarDemo extends StatelessWidget { + const CupertinoNavigationBarDemo({super.key}); + + static const String homeRoute = '/home'; + static const String secondPageRoute = '/home/item'; + + @override + Widget build(BuildContext context) { + return Navigator( + restorationScopeId: 'navigator', + initialRoute: CupertinoNavigationBarDemo.homeRoute, + onGenerateRoute: (RouteSettings settings) { + switch (settings.name) { + case CupertinoNavigationBarDemo.homeRoute: + return _NoAnimationCupertinoPageRoute( + title: GalleryLocalizations.of(context)! + .demoCupertinoNavigationBarTitle, + settings: settings, + builder: (BuildContext context) => _FirstPage(), + ); + case CupertinoNavigationBarDemo.secondPageRoute: + final Map arguments = settings.arguments! as Map; + final String? title = arguments['pageTitle'] as String?; + return CupertinoPageRoute( + title: title, + settings: settings, + builder: (BuildContext context) => _SecondPage(), + ); + } + return null; + }, + ); + } +} + +class _FirstPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return CupertinoPageScaffold( + child: CustomScrollView( + slivers: [ + const CupertinoSliverNavigationBar( + automaticallyImplyLeading: false, + ), + SliverPadding( + padding: + MediaQuery.of(context).removePadding(removeTop: true).padding, + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + final String title = GalleryLocalizations.of(context)! + .starterAppDrawerItem(index + 1); + return ListTile( + onTap: () { + Navigator.of(context).restorablePushNamed( + CupertinoNavigationBarDemo.secondPageRoute, + arguments: {'pageTitle': title}, + ); + }, + title: Text(title), + ); + }, + childCount: 20, + ), + ), + ), + ], + ), + ); + } +} + +class _SecondPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar(), + child: Container(), + ); + } +} + +/// A CupertinoPageRoute without any transition animations. +class _NoAnimationCupertinoPageRoute extends CupertinoPageRoute { + _NoAnimationCupertinoPageRoute({ + required super.builder, + super.settings, + super.title, + }); + + @override + Widget buildTransitions( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return child; + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_picker_demo.dart b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_picker_demo.dart new file mode 100644 index 0000000000..d71c539b22 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_picker_demo.dart @@ -0,0 +1,322 @@ +// 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:flutter/cupertino.dart'; +import 'package:intl/intl.dart'; + +import '../../gallery_localizations.dart'; + +// BEGIN cupertinoPickersDemo + +class CupertinoPickerDemo extends StatefulWidget { + const CupertinoPickerDemo({super.key}); + + @override + State createState() => _CupertinoPickerDemoState(); +} + +class _CupertinoPickerDemoState extends State { + Duration timer = Duration.zero; + + // Value that is shown in the date picker in date mode. + DateTime date = DateTime.now(); + + // Value that is shown in the date picker in time mode. + DateTime time = DateTime.now(); + + // Value that is shown in the date picker in dateAndTime mode. + DateTime dateTime = DateTime.now(); + + int _selectedWeekday = 0; + + static List getDaysOfWeek([String? locale]) { + final DateTime now = DateTime.now(); + final DateTime firstDayOfWeek = now.subtract(Duration(days: now.weekday - 1)); + return List.generate(7, (int index) => index) + .map((int value) => DateFormat(DateFormat.WEEKDAY, locale) + .format(firstDayOfWeek.add(Duration(days: value)))) + .toList(); + } + + void _showDemoPicker({ + required BuildContext context, + required Widget child, + }) { + final CupertinoThemeData themeData = CupertinoTheme.of(context); + final CupertinoTheme dialogBody = CupertinoTheme( + data: themeData, + child: child, + ); + + showCupertinoModalPopup( + context: context, + builder: (BuildContext context) => dialogBody, + ); + } + + Widget _buildDatePicker(BuildContext context) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + _showDemoPicker( + context: context, + child: _BottomPicker( + child: CupertinoDatePicker( + backgroundColor: + CupertinoColors.systemBackground.resolveFrom(context), + mode: CupertinoDatePickerMode.date, + initialDateTime: date, + onDateTimeChanged: (DateTime newDateTime) { + setState(() => date = newDateTime); + }, + ), + ), + ); + }, + child: _Menu( + children: [ + Text(GalleryLocalizations.of(context)!.demoCupertinoPickerDate), + Text( + DateFormat.yMMMMd().format(date), + style: const TextStyle(color: CupertinoColors.inactiveGray), + ), + ], + ), + ), + ); + } + + Widget _buildTimePicker(BuildContext context) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + _showDemoPicker( + context: context, + child: _BottomPicker( + child: CupertinoDatePicker( + backgroundColor: + CupertinoColors.systemBackground.resolveFrom(context), + mode: CupertinoDatePickerMode.time, + initialDateTime: time, + onDateTimeChanged: (DateTime newDateTime) { + setState(() => time = newDateTime); + }, + ), + ), + ); + }, + child: _Menu( + children: [ + Text(GalleryLocalizations.of(context)!.demoCupertinoPickerTime), + Text( + DateFormat.jm().format(time), + style: const TextStyle(color: CupertinoColors.inactiveGray), + ), + ], + ), + ), + ); + } + + Widget _buildDateAndTimePicker(BuildContext context) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + _showDemoPicker( + context: context, + child: _BottomPicker( + child: CupertinoDatePicker( + backgroundColor: + CupertinoColors.systemBackground.resolveFrom(context), + initialDateTime: dateTime, + onDateTimeChanged: (DateTime newDateTime) { + setState(() => dateTime = newDateTime); + }, + ), + ), + ); + }, + child: _Menu( + children: [ + Text(GalleryLocalizations.of(context)!.demoCupertinoPickerDateTime), + Flexible( + child: Text( + DateFormat.yMMMd().add_jm().format(dateTime), + style: const TextStyle(color: CupertinoColors.inactiveGray), + ), + ), + ], + ), + ), + ); + } + + Widget _buildCountdownTimerPicker(BuildContext context) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + _showDemoPicker( + context: context, + child: _BottomPicker( + child: CupertinoTimerPicker( + backgroundColor: + CupertinoColors.systemBackground.resolveFrom(context), + initialTimerDuration: timer, + onTimerDurationChanged: (Duration newTimer) { + setState(() => timer = newTimer); + }, + ), + ), + ); + }, + child: _Menu( + children: [ + Text(GalleryLocalizations.of(context)!.demoCupertinoPickerTimer), + Text( + '${timer.inHours}:' + '${(timer.inMinutes % 60).toString().padLeft(2, '0')}:' + '${(timer.inSeconds % 60).toString().padLeft(2, '0')}', + style: const TextStyle(color: CupertinoColors.inactiveGray), + ), + ], + ), + ), + ); + } + + Widget _buildPicker(BuildContext context) { + final String? locale = GalleryLocalizations.of(context)?.localeName; + final List daysOfWeek = getDaysOfWeek(locale); + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + _showDemoPicker( + context: context, + child: _BottomPicker( + child: CupertinoPicker( + backgroundColor: + CupertinoColors.systemBackground.resolveFrom(context), + itemExtent: 32.0, + magnification: 1.22, + squeeze: 1.2, + useMagnifier: true, + // This is called when selected item is changed. + onSelectedItemChanged: (int selectedItem) { + setState(() { + _selectedWeekday = selectedItem; + }); + }, + children: List.generate(daysOfWeek.length, (int index) { + return Center( + child: Text( + daysOfWeek[index], + ), + ); + }), + ), + ), + ); + }, + child: _Menu( + children: [ + Text(GalleryLocalizations.of(context)!.demoCupertinoPicker), + Text( + daysOfWeek[_selectedWeekday], + style: const TextStyle(color: CupertinoColors.inactiveGray), + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + automaticallyImplyLeading: false, + middle: + Text(GalleryLocalizations.of(context)!.demoCupertinoPickerTitle), + ), + child: DefaultTextStyle( + style: CupertinoTheme.of(context).textTheme.textStyle, + child: ListView( + children: [ + const SizedBox(height: 32), + _buildDatePicker(context), + _buildTimePicker(context), + _buildDateAndTimePicker(context), + _buildCountdownTimerPicker(context), + _buildPicker(context), + ], + ), + ), + ); + } +} + +class _BottomPicker extends StatelessWidget { + const _BottomPicker({required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Container( + height: 216, + padding: const EdgeInsets.only(top: 6), + margin: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + color: CupertinoColors.systemBackground.resolveFrom(context), + child: DefaultTextStyle( + style: TextStyle( + color: CupertinoColors.label.resolveFrom(context), + fontSize: 22, + ), + child: GestureDetector( + // Blocks taps from propagating to the modal sheet and popping. + onTap: () {}, + child: SafeArea( + top: false, + child: child, + ), + ), + ), + ); + } +} + +class _Menu extends StatelessWidget { + const _Menu({required this.children}); + + final List children; + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + border: Border( + top: BorderSide(color: CupertinoColors.inactiveGray, width: 0), + bottom: BorderSide(color: CupertinoColors.inactiveGray, width: 0), + ), + ), + height: 44, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: children, + ), + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_scrollbar_demo.dart b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_scrollbar_demo.dart new file mode 100644 index 0000000000..53f0e43181 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_scrollbar_demo.dart @@ -0,0 +1,40 @@ +// 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:flutter/cupertino.dart'; +import '../../gallery_localizations.dart'; + +// BEGIN cupertinoScrollbarDemo + +class CupertinoScrollbarDemo extends StatelessWidget { + const CupertinoScrollbarDemo({super.key}); + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + automaticallyImplyLeading: false, + middle: Text(localizations.demoCupertinoScrollbarTitle), + ), + child: CupertinoScrollbar( + thickness: 6.0, + thicknessWhileDragging: 10.0, + radius: const Radius.circular(34.0), + radiusWhileDragging: Radius.zero, + child: ListView.builder( + itemCount: 120, + itemBuilder: (BuildContext context, int index) { + return Center( + child: Text('item $index', + style: CupertinoTheme.of(context).textTheme.textStyle), + ); + }, + ), + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_search_text_field_demo.dart b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_search_text_field_demo.dart new file mode 100644 index 0000000000..611b749c02 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_search_text_field_demo.dart @@ -0,0 +1,107 @@ +// 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:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import '../../gallery_localizations.dart'; + +// BEGIN cupertinoSearchTextFieldDemo + +class CupertinoSearchTextFieldDemo extends StatefulWidget { + const CupertinoSearchTextFieldDemo({super.key}); + + @override + State createState() => + _CupertinoSearchTextFieldDemoState(); +} + +class _CupertinoSearchTextFieldDemoState + extends State { + final List platforms = [ + 'Android', + 'iOS', + 'Windows', + 'Linux', + 'MacOS', + 'Web' + ]; + + final TextEditingController _queryTextController = TextEditingController(); + String _searchPlatform = ''; + List filteredPlatforms = []; + + @override + void initState() { + super.initState(); + filteredPlatforms = platforms; + _queryTextController.addListener(() { + if (_queryTextController.text.isEmpty) { + setState(() { + _searchPlatform = ''; + filteredPlatforms = platforms; + }); + } else { + setState(() { + _searchPlatform = _queryTextController.text; + }); + } + }); + } + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + automaticallyImplyLeading: false, + middle: Text(localizations.demoCupertinoSearchTextFieldTitle), + ), + child: SafeArea( + child: Column( + children: [ + CupertinoSearchTextField( + controller: _queryTextController, + restorationId: 'search_text_field', + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 12), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + width: 0, + color: CupertinoColors.inactiveGray, + ), + ), + ), + placeholder: + localizations.demoCupertinoSearchTextFieldPlaceholder, + ), + _buildPlatformList(), + ], + ), + ), + ); + } + + Widget _buildPlatformList() { + if (_searchPlatform.isNotEmpty) { + final List tempList = []; + for (int i = 0; i < filteredPlatforms.length; i++) { + if (filteredPlatforms[i] + .toLowerCase() + .contains(_searchPlatform.toLowerCase())) { + tempList.add(filteredPlatforms[i]); + } + } + filteredPlatforms = tempList; + } + return ListView.builder( + itemCount: filteredPlatforms.length, + shrinkWrap: true, + itemBuilder: (BuildContext context, int index) { + return ListTile(title: Text(filteredPlatforms[index])); + }, + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_segmented_control_demo.dart b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_segmented_control_demo.dart new file mode 100644 index 0000000000..51ad30db4a --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_segmented_control_demo.dart @@ -0,0 +1,96 @@ +// 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:flutter/cupertino.dart'; + +import '../../gallery_localizations.dart'; + +// BEGIN cupertinoSegmentedControlDemo + +class CupertinoSegmentedControlDemo extends StatefulWidget { + const CupertinoSegmentedControlDemo({super.key}); + + @override + State createState() => + _CupertinoSegmentedControlDemoState(); +} + +class _CupertinoSegmentedControlDemoState + extends State with RestorationMixin { + RestorableInt currentSegment = RestorableInt(0); + + @override + String get restorationId => 'cupertino_segmented_control'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(currentSegment, 'current_segment'); + } + + void onValueChanged(int? newValue) { + setState(() { + currentSegment.value = newValue!; + }); + } + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + const double segmentedControlMaxWidth = 500.0; + final Map children = { + 0: Text(localizations.colorsIndigo), + 1: Text(localizations.colorsTeal), + 2: Text(localizations.colorsCyan), + }; + + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + automaticallyImplyLeading: false, + middle: Text( + localizations.demoCupertinoSegmentedControlTitle, + ), + ), + child: DefaultTextStyle( + style: CupertinoTheme.of(context) + .textTheme + .textStyle + .copyWith(fontSize: 13), + child: SafeArea( + child: ListView( + children: [ + const SizedBox(height: 16), + SizedBox( + width: segmentedControlMaxWidth, + child: CupertinoSegmentedControl( + children: children, + onValueChanged: onValueChanged, + groupValue: currentSegment.value, + ), + ), + SizedBox( + width: segmentedControlMaxWidth, + child: Padding( + padding: const EdgeInsets.all(16), + child: CupertinoSlidingSegmentedControl( + children: children, + onValueChanged: onValueChanged, + groupValue: currentSegment.value, + ), + ), + ), + Container( + padding: const EdgeInsets.all(16), + height: 300, + alignment: Alignment.center, + child: children[currentSegment.value], + ), + ], + ), + ), + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_slider_demo.dart b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_slider_demo.dart new file mode 100644 index 0000000000..dff7312ade --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_slider_demo.dart @@ -0,0 +1,110 @@ +// 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:flutter/cupertino.dart'; +import '../../gallery_localizations.dart'; + +// BEGIN cupertinoSliderDemo + +class CupertinoSliderDemo extends StatefulWidget { + const CupertinoSliderDemo({super.key}); + + @override + State createState() => _CupertinoSliderDemoState(); +} + +class _CupertinoSliderDemoState extends State + with RestorationMixin { + final RestorableDouble _value = RestorableDouble(25.0); + final RestorableDouble _discreteValue = RestorableDouble(20.0); + + @override + String get restorationId => 'cupertino_slider_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_value, 'value'); + registerForRestoration(_discreteValue, 'discrete_value'); + } + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + automaticallyImplyLeading: false, + middle: Text(localizations.demoCupertinoSliderTitle), + ), + child: DefaultTextStyle( + style: CupertinoTheme.of(context).textTheme.textStyle, + child: Center( + child: Wrap( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 32), + CupertinoSlider( + value: _value.value, + max: 100.0, + onChanged: (double value) { + setState(() { + _value.value = value; + }); + }, + ), + CupertinoSlider( + value: _value.value, + max: 100.0, + onChanged: null, + ), + MergeSemantics( + child: Text( + localizations.demoCupertinoSliderContinuous( + _value.value.toStringAsFixed(1), + ), + ), + ), + ], + ), + // Disabled sliders + // TODO(guidezpl): See https://github.com/flutter/flutter/issues/106691 + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 32), + CupertinoSlider( + value: _discreteValue.value, + max: 100.0, + divisions: 5, + onChanged: (double value) { + setState(() { + _discreteValue.value = value; + }); + }, + ), + CupertinoSlider( + value: _discreteValue.value, + max: 100.0, + divisions: 5, + onChanged: null, + ), + MergeSemantics( + child: Text( + localizations.demoCupertinoSliderDiscrete( + _discreteValue.value.toStringAsFixed(1), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_switch_demo.dart b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_switch_demo.dart new file mode 100644 index 0000000000..1ed3b25003 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_switch_demo.dart @@ -0,0 +1,91 @@ +// 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:flutter/cupertino.dart'; +import '../../gallery_localizations.dart'; + +// BEGIN cupertinoSwitchDemo + +class CupertinoSwitchDemo extends StatefulWidget { + const CupertinoSwitchDemo({super.key}); + + @override + State createState() => _CupertinoSwitchDemoState(); +} + +class _CupertinoSwitchDemoState extends State + with RestorationMixin { + final RestorableBool _switchValueA = RestorableBool(false); + final RestorableBool _switchValueB = RestorableBool(true); + + @override + String get restorationId => 'cupertino_switch_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_switchValueA, 'switch_valueA'); + registerForRestoration(_switchValueB, 'switch_valueB'); + } + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + automaticallyImplyLeading: false, + middle: Text( + localizations.demoSelectionControlsSwitchTitle, + ), + ), + child: Center( + child: Semantics( + container: true, + label: localizations.demoSelectionControlsSwitchTitle, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CupertinoSwitch( + value: _switchValueA.value, + onChanged: (bool value) { + setState(() { + _switchValueA.value = value; + }); + }, + ), + CupertinoSwitch( + value: _switchValueB.value, + onChanged: (bool value) { + setState(() { + _switchValueB.value = value; + }); + }, + ), + ], + ), + // Disabled switches + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CupertinoSwitch( + value: _switchValueA.value, + onChanged: null, + ), + CupertinoSwitch( + value: _switchValueB.value, + onChanged: null, + ), + ], + ), + ], + ), + ), + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_tab_bar_demo.dart b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_tab_bar_demo.dart new file mode 100644 index 0000000000..3aa4b70982 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_tab_bar_demo.dart @@ -0,0 +1,92 @@ +// 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:flutter/cupertino.dart'; + +import '../../gallery_localizations.dart'; + +// BEGIN cupertinoNavigationDemo + +class _TabInfo { + const _TabInfo(this.title, this.icon); + + final String title; + final IconData icon; +} + +class CupertinoTabBarDemo extends StatelessWidget { + const CupertinoTabBarDemo({super.key}); + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + final List<_TabInfo> tabInfo = <_TabInfo>[ + _TabInfo( + localizations.cupertinoTabBarHomeTab, + CupertinoIcons.home, + ), + _TabInfo( + localizations.cupertinoTabBarChatTab, + CupertinoIcons.conversation_bubble, + ), + _TabInfo( + localizations.cupertinoTabBarProfileTab, + CupertinoIcons.profile_circled, + ), + ]; + + return DefaultTextStyle( + style: CupertinoTheme.of(context).textTheme.textStyle, + child: CupertinoTabScaffold( + restorationId: 'cupertino_tab_scaffold', + tabBar: CupertinoTabBar( + items: [ + for (final _TabInfo tabInfo in tabInfo) + BottomNavigationBarItem( + label: tabInfo.title, + icon: Icon(tabInfo.icon), + ), + ], + ), + tabBuilder: (BuildContext context, int index) { + return CupertinoTabView( + restorationScopeId: 'cupertino_tab_view_$index', + builder: (BuildContext context) => _CupertinoDemoTab( + title: tabInfo[index].title, + icon: tabInfo[index].icon, + ), + defaultTitle: tabInfo[index].title, + ); + }, + ), + ); + } +} + +class _CupertinoDemoTab extends StatelessWidget { + const _CupertinoDemoTab({ + required this.title, + required this.icon, + }); + + final String title; + final IconData icon; + + @override + Widget build(BuildContext context) { + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar(), + backgroundColor: CupertinoColors.systemBackground, + child: Center( + child: Icon( + icon, + semanticLabel: title, + size: 100, + ), + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_text_field_demo.dart b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_text_field_demo.dart new file mode 100644 index 0000000000..7cf19ce337 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/cupertino/cupertino_text_field_demo.dart @@ -0,0 +1,88 @@ +// 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:flutter/cupertino.dart'; +import '../../gallery_localizations.dart'; + +// BEGIN cupertinoTextFieldDemo + +class CupertinoTextFieldDemo extends StatelessWidget { + const CupertinoTextFieldDemo({super.key}); + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + automaticallyImplyLeading: false, + middle: Text(localizations.demoCupertinoTextFieldTitle), + ), + child: SafeArea( + child: ListView( + restorationId: 'text_field_demo_list_view', + padding: const EdgeInsets.all(16), + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: CupertinoTextField( + textInputAction: TextInputAction.next, + restorationId: 'email_address_text_field', + placeholder: localizations.demoTextFieldEmail, + keyboardType: TextInputType.emailAddress, + clearButtonMode: OverlayVisibilityMode.editing, + autocorrect: false, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: CupertinoTextField( + textInputAction: TextInputAction.next, + restorationId: 'login_password_text_field', + placeholder: localizations.rallyLoginPassword, + clearButtonMode: OverlayVisibilityMode.editing, + obscureText: true, + autocorrect: false, + ), + ), + // Disabled text field + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: CupertinoTextField( + enabled: false, + textInputAction: TextInputAction.next, + restorationId: 'login_password_text_field_disabled', + placeholder: localizations.rallyLoginPassword, + clearButtonMode: OverlayVisibilityMode.editing, + obscureText: true, + autocorrect: false, + ), + ), + CupertinoTextField( + textInputAction: TextInputAction.done, + restorationId: 'pin_number_text_field', + prefix: const Icon( + CupertinoIcons.padlock_solid, + size: 28, + ), + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 12), + clearButtonMode: OverlayVisibilityMode.editing, + keyboardType: TextInputType.number, + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + width: 0, + color: CupertinoColors.inactiveGray, + ), + ), + ), + placeholder: localizations.demoCupertinoTextFieldPIN, + ), + ], + ), + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/cupertino/demo_types.dart b/dev/integration_tests/new_gallery/lib/demos/cupertino/demo_types.dart new file mode 100644 index 0000000000..04e345e2c9 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/cupertino/demo_types.dart @@ -0,0 +1,11 @@ +// 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. + +enum AlertDemoType { + alert, + alertTitle, + alertButtons, + alertButtonsOnly, + actionSheet, +} diff --git a/dev/integration_tests/new_gallery/lib/demos/material/app_bar_demo.dart b/dev/integration_tests/new_gallery/lib/demos/material/app_bar_demo.dart new file mode 100644 index 0000000000..8d83f7f83c --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/app_bar_demo.dart @@ -0,0 +1,73 @@ +// 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:flutter/material.dart'; +import '../../gallery_localizations.dart'; + +// BEGIN appbarDemo + +class AppBarDemo extends StatelessWidget { + const AppBarDemo({super.key}); + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localization = GalleryLocalizations.of(context)!; + return Scaffold( + appBar: AppBar( + leading: IconButton( + tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip, + icon: const Icon(Icons.menu), + onPressed: () {}, + ), + title: Text( + localization.demoAppBarTitle, + ), + actions: [ + IconButton( + tooltip: localization.starterAppTooltipFavorite, + icon: const Icon( + Icons.favorite, + ), + onPressed: () {}, + ), + IconButton( + tooltip: localization.starterAppTooltipSearch, + icon: const Icon( + Icons.search, + ), + onPressed: () {}, + ), + PopupMenuButton( + itemBuilder: (BuildContext context) { + return >[ + PopupMenuItem( + child: Text( + localization.demoNavigationRailFirst, + ), + ), + PopupMenuItem( + child: Text( + localization.demoNavigationRailSecond, + ), + ), + PopupMenuItem( + child: Text( + localization.demoNavigationRailThird, + ), + ), + ]; + }, + ) + ], + ), + body: Center( + child: Text( + localization.cupertinoTabBarHomeTab, + ), + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/material/banner_demo.dart b/dev/integration_tests/new_gallery/lib/demos/material/banner_demo.dart new file mode 100644 index 0000000000..fb614ba187 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/banner_demo.dart @@ -0,0 +1,143 @@ +// 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:flutter/material.dart'; +import '../../gallery_localizations.dart'; + +// BEGIN bannerDemo + +enum BannerDemoAction { + reset, + showMultipleActions, + showLeading, +} + +class BannerDemo extends StatefulWidget { + const BannerDemo({super.key}); + + @override + State createState() => _BannerDemoState(); +} + +class _BannerDemoState extends State with RestorationMixin { + static const int _itemCount = 20; + + @override + String get restorationId => 'banner_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_displayBanner, 'display_banner'); + registerForRestoration(_showMultipleActions, 'show_multiple_actions'); + registerForRestoration(_showLeading, 'show_leading'); + } + + final RestorableBool _displayBanner = RestorableBool(true); + final RestorableBool _showMultipleActions = RestorableBool(true); + final RestorableBool _showLeading = RestorableBool(true); + + @override + void dispose() { + _displayBanner.dispose(); + _showMultipleActions.dispose(); + _showLeading.dispose(); + super.dispose(); + } + + void handleDemoAction(BannerDemoAction action) { + setState(() { + switch (action) { + case BannerDemoAction.reset: + _displayBanner.value = true; + _showMultipleActions.value = true; + _showLeading.value = true; + case BannerDemoAction.showMultipleActions: + _showMultipleActions.value = !_showMultipleActions.value; + case BannerDemoAction.showLeading: + _showLeading.value = !_showLeading.value; + } + }); + } + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + final MaterialBanner banner = MaterialBanner( + content: Text(localizations.bannerDemoText), + leading: _showLeading.value + ? CircleAvatar( + backgroundColor: colorScheme.primary, + child: Icon(Icons.access_alarm, color: colorScheme.onPrimary), + ) + : null, + actions: [ + TextButton( + onPressed: () { + setState(() { + _displayBanner.value = false; + }); + }, + child: Text(localizations.signIn), + ), + if (_showMultipleActions.value) + TextButton( + onPressed: () { + setState(() { + _displayBanner.value = false; + }); + }, + child: Text(localizations.dismiss), + ), + ], + backgroundColor: colorScheme.background, + ); + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(localizations.demoBannerTitle), + actions: [ + PopupMenuButton( + onSelected: handleDemoAction, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: BannerDemoAction.reset, + child: Text(localizations.bannerDemoResetText), + ), + const PopupMenuDivider(), + CheckedPopupMenuItem( + value: BannerDemoAction.showMultipleActions, + checked: _showMultipleActions.value, + child: Text(localizations.bannerDemoMultipleText), + ), + CheckedPopupMenuItem( + value: BannerDemoAction.showLeading, + checked: _showLeading.value, + child: Text(localizations.bannerDemoLeadingText), + ), + ], + ), + ], + ), + body: ListView.builder( + restorationId: 'banner_demo_list_view', + itemCount: _displayBanner.value ? _itemCount + 1 : _itemCount, + itemBuilder: (BuildContext context, int index) { + if (index == 0 && _displayBanner.value) { + return banner; + } + return ListTile( + title: Text( + localizations.starterAppDrawerItem( + _displayBanner.value ? index : index + 1), + ), + ); + }, + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/material/bottom_app_bar_demo.dart b/dev/integration_tests/new_gallery/lib/demos/material/bottom_app_bar_demo.dart new file mode 100644 index 0000000000..e6ec26e7a5 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/bottom_app_bar_demo.dart @@ -0,0 +1,201 @@ +// 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:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import '../../gallery_localizations.dart'; + +// BEGIN bottomAppBarDemo + +class BottomAppBarDemo extends StatefulWidget { + const BottomAppBarDemo({super.key}); + + @override + State createState() => _BottomAppBarDemoState(); +} + +class _BottomAppBarDemoState extends State + with RestorationMixin { + final RestorableBool _showFab = RestorableBool(true); + final RestorableBool _showNotch = RestorableBool(true); + final RestorableInt _currentFabLocation = RestorableInt(0); + + @override + String get restorationId => 'bottom_app_bar_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_showFab, 'show_fab'); + registerForRestoration(_showNotch, 'show_notch'); + registerForRestoration(_currentFabLocation, 'fab_location'); + } + + @override + void dispose() { + _showFab.dispose(); + _showNotch.dispose(); + _currentFabLocation.dispose(); + super.dispose(); + } + + // Since FloatingActionButtonLocation is not an enum, the index of the + // selected FloatingActionButtonLocation is used for state restoration. + static const List _fabLocations = [ + FloatingActionButtonLocation.endDocked, + FloatingActionButtonLocation.centerDocked, + FloatingActionButtonLocation.endFloat, + FloatingActionButtonLocation.centerFloat, + ]; + + void _onShowNotchChanged(bool value) { + setState(() { + _showNotch.value = value; + }); + } + + void _onShowFabChanged(bool value) { + setState(() { + _showFab.value = value; + }); + } + + void _onFabLocationChanged(int? value) { + setState(() { + _currentFabLocation.value = value!; + }); + } + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(localizations.demoBottomAppBarTitle), + ), + body: ListView( + padding: const EdgeInsets.only(bottom: 88), + children: [ + SwitchListTile( + title: Text( + localizations.demoFloatingButtonTitle, + ), + value: _showFab.value, + onChanged: _onShowFabChanged, + ), + SwitchListTile( + title: Text(localizations.bottomAppBarNotch), + value: _showNotch.value, + onChanged: _onShowNotchChanged, + ), + Padding( + padding: const EdgeInsets.all(16), + child: Text(localizations.bottomAppBarPosition), + ), + RadioListTile( + title: Text( + localizations.bottomAppBarPositionDockedEnd, + ), + value: 0, + groupValue: _currentFabLocation.value, + onChanged: _onFabLocationChanged, + ), + RadioListTile( + title: Text( + localizations.bottomAppBarPositionDockedCenter, + ), + value: 1, + groupValue: _currentFabLocation.value, + onChanged: _onFabLocationChanged, + ), + RadioListTile( + title: Text( + localizations.bottomAppBarPositionFloatingEnd, + ), + value: 2, + groupValue: _currentFabLocation.value, + onChanged: _onFabLocationChanged, + ), + RadioListTile( + title: Text( + localizations.bottomAppBarPositionFloatingCenter, + ), + value: 3, + groupValue: _currentFabLocation.value, + onChanged: _onFabLocationChanged, + ), + ], + ), + floatingActionButton: _showFab.value + ? Semantics( + container: true, + sortKey: const OrdinalSortKey(0), + child: FloatingActionButton( + onPressed: () {}, + tooltip: localizations.buttonTextCreate, + child: const Icon(Icons.add), + ), + ) + : null, + floatingActionButtonLocation: _fabLocations[_currentFabLocation.value], + bottomNavigationBar: _DemoBottomAppBar( + fabLocation: _fabLocations[_currentFabLocation.value], + shape: _showNotch.value ? const CircularNotchedRectangle() : null, + ), + ); + } +} + +class _DemoBottomAppBar extends StatelessWidget { + const _DemoBottomAppBar({ + required this.fabLocation, + this.shape, + }); + + final FloatingActionButtonLocation fabLocation; + final NotchedShape? shape; + + static final List centerLocations = [ + FloatingActionButtonLocation.centerDocked, + FloatingActionButtonLocation.centerFloat, + ]; + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return Semantics( + sortKey: const OrdinalSortKey(1), + container: true, + label: localizations.bottomAppBar, + child: BottomAppBar( + shape: shape, + child: IconTheme( + data: IconThemeData(color: Theme.of(context).colorScheme.onPrimary), + child: Row( + children: [ + IconButton( + tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip, + icon: const Icon(Icons.menu), + onPressed: () {}, + ), + if (centerLocations.contains(fabLocation)) const Spacer(), + IconButton( + tooltip: localizations.starterAppTooltipSearch, + icon: const Icon(Icons.search), + onPressed: () {}, + ), + IconButton( + tooltip: localizations.starterAppTooltipFavorite, + icon: const Icon(Icons.favorite), + onPressed: () {}, + ) + ], + ), + ), + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/material/bottom_navigation_demo.dart b/dev/integration_tests/new_gallery/lib/demos/material/bottom_navigation_demo.dart new file mode 100644 index 0000000000..3f272d4e71 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/bottom_navigation_demo.dart @@ -0,0 +1,179 @@ +// 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 '../../gallery_localizations.dart'; +import 'material_demo_types.dart'; + +// BEGIN bottomNavigationDemo + +class BottomNavigationDemo extends StatefulWidget { + const BottomNavigationDemo({ + super.key, + required this.restorationId, + required this.type, + }); + + final String restorationId; + final BottomNavigationDemoType type; + + @override + State createState() => _BottomNavigationDemoState(); +} + +class _BottomNavigationDemoState extends State + with RestorationMixin { + final RestorableInt _currentIndex = RestorableInt(0); + + @override + String get restorationId => widget.restorationId; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_currentIndex, 'bottom_navigation_tab_index'); + } + + @override + void dispose() { + _currentIndex.dispose(); + super.dispose(); + } + + String _title(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + switch (widget.type) { + case BottomNavigationDemoType.withLabels: + return localizations.demoBottomNavigationPersistentLabels; + case BottomNavigationDemoType.withoutLabels: + return localizations.demoBottomNavigationSelectedLabel; + } + } + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + final TextTheme textTheme = Theme.of(context).textTheme; + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + List bottomNavigationBarItems = [ + BottomNavigationBarItem( + icon: const Icon(Icons.add_comment), + label: localizations.bottomNavigationCommentsTab, + ), + BottomNavigationBarItem( + icon: const Icon(Icons.calendar_today), + label: localizations.bottomNavigationCalendarTab, + ), + BottomNavigationBarItem( + icon: const Icon(Icons.account_circle), + label: localizations.bottomNavigationAccountTab, + ), + BottomNavigationBarItem( + icon: const Icon(Icons.alarm_on), + label: localizations.bottomNavigationAlarmTab, + ), + BottomNavigationBarItem( + icon: const Icon(Icons.camera_enhance), + label: localizations.bottomNavigationCameraTab, + ), + ]; + + if (widget.type == BottomNavigationDemoType.withLabels) { + bottomNavigationBarItems = bottomNavigationBarItems.sublist( + 0, bottomNavigationBarItems.length - 2); + _currentIndex.value = _currentIndex.value + .clamp(0, bottomNavigationBarItems.length - 1) + ; + } + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(_title(context)), + ), + body: Center( + child: PageTransitionSwitcher( + transitionBuilder: (Widget child, Animation animation, Animation secondaryAnimation) { + return FadeThroughTransition( + animation: animation, + secondaryAnimation: secondaryAnimation, + child: child, + ); + }, + child: _NavigationDestinationView( + // Adding [UniqueKey] to make sure the widget rebuilds when transitioning. + key: UniqueKey(), + item: bottomNavigationBarItems[_currentIndex.value], + ), + ), + ), + bottomNavigationBar: BottomNavigationBar( + showUnselectedLabels: + widget.type == BottomNavigationDemoType.withLabels, + items: bottomNavigationBarItems, + currentIndex: _currentIndex.value, + type: BottomNavigationBarType.fixed, + selectedFontSize: textTheme.bodySmall!.fontSize!, + unselectedFontSize: textTheme.bodySmall!.fontSize!, + onTap: (int index) { + setState(() { + _currentIndex.value = index; + }); + }, + selectedItemColor: colorScheme.onPrimary, + unselectedItemColor: colorScheme.onPrimary.withOpacity(0.38), + backgroundColor: colorScheme.primary, + ), + ); + } +} + +class _NavigationDestinationView extends StatelessWidget { + const _NavigationDestinationView({ + super.key, + required this.item, + }); + + final BottomNavigationBarItem item; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + ExcludeSemantics( + child: Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.asset( + 'assets/demos/bottom_navigation_background.png', + package: 'flutter_gallery_assets', + ), + ), + ), + ), + ), + Center( + child: IconTheme( + data: const IconThemeData( + color: Colors.white, + size: 80, + ), + child: Semantics( + label: GalleryLocalizations.of(context)! + .bottomNavigationContentPlaceholder( + item.label!, + ), + child: item.icon, + ), + ), + ), + ], + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/material/bottom_sheet_demo.dart b/dev/integration_tests/new_gallery/lib/demos/material/bottom_sheet_demo.dart new file mode 100644 index 0000000000..5b029822e1 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/bottom_sheet_demo.dart @@ -0,0 +1,189 @@ +// 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:flutter/material.dart'; + +import '../../gallery_localizations.dart'; +import 'material_demo_types.dart'; + +class BottomSheetDemo extends StatelessWidget { + const BottomSheetDemo({ + super.key, + required this.type, + }); + + final BottomSheetDemoType type; + + String _title(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + switch (type) { + case BottomSheetDemoType.persistent: + return localizations.demoBottomSheetPersistentTitle; + case BottomSheetDemoType.modal: + return localizations.demoBottomSheetModalTitle; + } + } + + Widget _bottomSheetDemo(BuildContext context) { + switch (type) { + case BottomSheetDemoType.persistent: + return _PersistentBottomSheetDemo(); + case BottomSheetDemoType.modal: + return _ModalBottomSheetDemo(); + } + } + + @override + Widget build(BuildContext context) { + // We wrap the demo in a [Navigator] to make sure that the modal bottom + // sheets gets dismissed when changing demo. + return Navigator( + // Adding [ValueKey] to make sure that the widget gets rebuilt when + // changing type. + key: ValueKey(type), + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + builder: (BuildContext context) => Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(_title(context)), + ), + floatingActionButton: FloatingActionButton( + onPressed: () {}, + backgroundColor: Theme.of(context).colorScheme.secondary, + child: Icon( + Icons.add, + semanticLabel: + GalleryLocalizations.of(context)!.demoBottomSheetAddLabel, + ), + ), + body: _bottomSheetDemo(context), + ), + ); + }, + ); + } +} + +// BEGIN bottomSheetDemoModal#1 bottomSheetDemoPersistent#1 + +class _BottomSheetContent extends StatelessWidget { + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return SizedBox( + height: 300, + child: Column( + children: [ + SizedBox( + height: 70, + child: Center( + child: Text( + localizations.demoBottomSheetHeader, + textAlign: TextAlign.center, + ), + ), + ), + const Divider(thickness: 1), + Expanded( + child: ListView.builder( + itemCount: 21, + itemBuilder: (BuildContext context, int index) { + return ListTile( + title: Text(localizations.demoBottomSheetItem(index)), + ); + }, + ), + ), + ], + ), + ); + } +} + +// END bottomSheetDemoModal#1 bottomSheetDemoPersistent#1 + +// BEGIN bottomSheetDemoModal#2 + +class _ModalBottomSheetDemo extends StatelessWidget { + void _showModalBottomSheet(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return _BottomSheetContent(); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Center( + child: ElevatedButton( + onPressed: () { + _showModalBottomSheet(context); + }, + child: + Text(GalleryLocalizations.of(context)!.demoBottomSheetButtonText), + ), + ); + } +} + +// END + +// BEGIN bottomSheetDemoPersistent#2 + +class _PersistentBottomSheetDemo extends StatefulWidget { + @override + _PersistentBottomSheetDemoState createState() => + _PersistentBottomSheetDemoState(); +} + +class _PersistentBottomSheetDemoState + extends State<_PersistentBottomSheetDemo> { + VoidCallback? _showBottomSheetCallback; + + @override + void initState() { + super.initState(); + _showBottomSheetCallback = _showPersistentBottomSheet; + } + + void _showPersistentBottomSheet() { + setState(() { + // Disable the show bottom sheet button. + _showBottomSheetCallback = null; + }); + + Scaffold.of(context) + .showBottomSheet( + (BuildContext context) { + return _BottomSheetContent(); + }, + elevation: 25, + ) + .closed + .whenComplete(() { + if (mounted) { + setState(() { + // Re-enable the bottom sheet button. + _showBottomSheetCallback = _showPersistentBottomSheet; + }); + } + }); + } + + @override + Widget build(BuildContext context) { + return Center( + child: ElevatedButton( + onPressed: _showBottomSheetCallback, + child: + Text(GalleryLocalizations.of(context)!.demoBottomSheetButtonText), + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/material/button_demo.dart b/dev/integration_tests/new_gallery/lib/demos/material/button_demo.dart new file mode 100644 index 0000000000..9755b6f816 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/button_demo.dart @@ -0,0 +1,297 @@ +// 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:flutter/material.dart'; +import '../../gallery_localizations.dart'; +import 'material_demo_types.dart'; + +class ButtonDemo extends StatelessWidget { + const ButtonDemo({super.key, required this.type}); + + final ButtonDemoType type; + + String _title(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + switch (type) { + case ButtonDemoType.text: + return localizations.demoTextButtonTitle; + case ButtonDemoType.elevated: + return localizations.demoElevatedButtonTitle; + case ButtonDemoType.outlined: + return localizations.demoOutlinedButtonTitle; + case ButtonDemoType.toggle: + return localizations.demoToggleButtonTitle; + case ButtonDemoType.floating: + return localizations.demoFloatingButtonTitle; + } + } + + @override + Widget build(BuildContext context) { + Widget? buttons; + switch (type) { + case ButtonDemoType.text: + buttons = _TextButtonDemo(); + case ButtonDemoType.elevated: + buttons = _ElevatedButtonDemo(); + case ButtonDemoType.outlined: + buttons = _OutlinedButtonDemo(); + case ButtonDemoType.toggle: + buttons = _ToggleButtonsDemo(); + case ButtonDemoType.floating: + buttons = _FloatingActionButtonDemo(); + } + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(_title(context)), + ), + body: buttons, + ); + } +} + +// BEGIN buttonDemoText + +class _TextButtonDemo extends StatelessWidget { + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: () {}, + child: Text(localizations.buttonText), + ), + const SizedBox(width: 12), + TextButton.icon( + icon: const Icon(Icons.add, size: 18), + label: Text(localizations.buttonText), + onPressed: () {}, + ), + ], + ), + const SizedBox(height: 12), + // Disabled buttons + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: null, + child: Text(localizations.buttonText), + ), + const SizedBox(width: 12), + TextButton.icon( + icon: const Icon(Icons.add, size: 18), + label: Text(localizations.buttonText), + onPressed: null, + ), + ], + ), + ], + ); + } +} + +// END + +// BEGIN buttonDemoElevated + +class _ElevatedButtonDemo extends StatelessWidget { + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () {}, + child: Text(localizations.buttonText), + ), + const SizedBox(width: 12), + ElevatedButton.icon( + icon: const Icon(Icons.add, size: 18), + label: Text(localizations.buttonText), + onPressed: () {}, + ), + ], + ), + const SizedBox(height: 12), + // Disabled buttons + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: null, + child: Text(localizations.buttonText), + ), + const SizedBox(width: 12), + ElevatedButton.icon( + icon: const Icon(Icons.add, size: 18), + label: Text(localizations.buttonText), + onPressed: null, + ), + ], + ), + ], + ); + } +} + +// END + +// BEGIN buttonDemoOutlined + +class _OutlinedButtonDemo extends StatelessWidget { + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + OutlinedButton( + onPressed: () {}, + child: Text(localizations.buttonText), + ), + const SizedBox(width: 12), + OutlinedButton.icon( + icon: const Icon(Icons.add, size: 18), + label: Text(localizations.buttonText), + onPressed: () {}, + ), + ], + ), + const SizedBox(height: 12), + // Disabled buttons + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + OutlinedButton( + onPressed: null, + child: Text(localizations.buttonText), + ), + const SizedBox(width: 12), + OutlinedButton.icon( + icon: const Icon(Icons.add, size: 18), + label: Text(localizations.buttonText), + onPressed: null, + ), + ], + ), + ], + ); + } +} + +// END + +// BEGIN buttonDemoToggle + +class _ToggleButtonsDemo extends StatefulWidget { + @override + _ToggleButtonsDemoState createState() => _ToggleButtonsDemoState(); +} + +class _ToggleButtonsDemoState extends State<_ToggleButtonsDemo> + with RestorationMixin { + final List isSelected = [ + RestorableBool(false), + RestorableBool(true), + RestorableBool(false), + ]; + + @override + String get restorationId => 'toggle_button_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(isSelected[0], 'first_item'); + registerForRestoration(isSelected[1], 'second_item'); + registerForRestoration(isSelected[2], 'third_item'); + } + + @override + void dispose() { + for (final RestorableBool restorableBool in isSelected) { + restorableBool.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ToggleButtons( + onPressed: (int index) { + setState(() { + isSelected[index].value = !isSelected[index].value; + }); + }, + isSelected: isSelected.map((RestorableBool element) => element.value).toList(), + children: const [ + Icon(Icons.format_bold), + Icon(Icons.format_italic), + Icon(Icons.format_underline), + ], + ), + const SizedBox(height: 12), + // Disabled toggle buttons + ToggleButtons( + isSelected: isSelected.map((RestorableBool element) => element.value).toList(), + children: const [ + Icon(Icons.format_bold), + Icon(Icons.format_italic), + Icon(Icons.format_underline), + ], + ), + ], + ), + ); + } +} + +// END + +// BEGIN buttonDemoFloating + +class _FloatingActionButtonDemo extends StatelessWidget { + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FloatingActionButton( + onPressed: () {}, + tooltip: localizations.buttonTextCreate, + child: const Icon(Icons.add), + ), + const SizedBox(width: 12), + FloatingActionButton.extended( + icon: const Icon(Icons.add), + label: Text(localizations.buttonTextCreate), + onPressed: () {}, + ), + ], + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/material/cards_demo.dart b/dev/integration_tests/new_gallery/lib/demos/material/cards_demo.dart new file mode 100644 index 0000000000..1bea3b337e --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/cards_demo.dart @@ -0,0 +1,441 @@ +// 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:flutter/material.dart'; +import '../../gallery_localizations.dart'; + +const String _kGalleryAssetsPackage = 'flutter_gallery_assets'; + +// BEGIN cardsDemo + +enum CardType { + standard, + tappable, + selectable, +} + +class TravelDestination { + const TravelDestination({ + required this.assetName, + required this.assetPackage, + required this.title, + required this.description, + required this.city, + required this.location, + this.cardType = CardType.standard, + }); + + final String assetName; + final String assetPackage; + final String title; + final String description; + final String city; + final String location; + final CardType cardType; +} + +List destinations(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + return [ + TravelDestination( + assetName: 'places/india_thanjavur_market.png', + assetPackage: _kGalleryAssetsPackage, + title: localizations.cardsDemoTravelDestinationTitle1, + description: localizations.cardsDemoTravelDestinationDescription1, + city: localizations.cardsDemoTravelDestinationCity1, + location: localizations.cardsDemoTravelDestinationLocation1, + ), + TravelDestination( + assetName: 'places/india_chettinad_silk_maker.png', + assetPackage: _kGalleryAssetsPackage, + title: localizations.cardsDemoTravelDestinationTitle2, + description: localizations.cardsDemoTravelDestinationDescription2, + city: localizations.cardsDemoTravelDestinationCity2, + location: localizations.cardsDemoTravelDestinationLocation2, + cardType: CardType.tappable, + ), + TravelDestination( + assetName: 'places/india_tanjore_thanjavur_temple.png', + assetPackage: _kGalleryAssetsPackage, + title: localizations.cardsDemoTravelDestinationTitle3, + description: localizations.cardsDemoTravelDestinationDescription3, + city: localizations.cardsDemoTravelDestinationCity1, + location: localizations.cardsDemoTravelDestinationLocation1, + cardType: CardType.selectable, + ), + ]; +} + +class TravelDestinationItem extends StatelessWidget { + const TravelDestinationItem( + {super.key, required this.destination, this.shape}); + + // This height will allow for all the Card's content to fit comfortably within the card. + static const double height = 360.0; + final TravelDestination destination; + final ShapeBorder? shape; + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + bottom: false, + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + SectionTitle( + title: GalleryLocalizations.of(context)! + .settingsTextScalingNormal), + SizedBox( + height: height, + child: Card( + // This ensures that the Card's children are clipped correctly. + clipBehavior: Clip.antiAlias, + shape: shape, + child: Semantics( + label: destination.title, + child: TravelDestinationContent(destination: destination), + ), + ), + ), + ], + ), + ), + ); + } +} + +class TappableTravelDestinationItem extends StatelessWidget { + const TappableTravelDestinationItem({ + super.key, + required this.destination, + this.shape, + }); + + // This height will allow for all the Card's content to fit comfortably within the card. + static const double height = 298.0; + final TravelDestination destination; + final ShapeBorder? shape; + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + bottom: false, + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + SectionTitle( + title: GalleryLocalizations.of(context)!.cardsDemoTappable), + SizedBox( + height: height, + child: Card( + // This ensures that the Card's children (including the ink splash) are clipped correctly. + clipBehavior: Clip.antiAlias, + shape: shape, + child: InkWell( + onTap: () {}, + // Generally, material cards use onSurface with 12% opacity for the pressed state. + splashColor: + Theme.of(context).colorScheme.onSurface.withOpacity(0.12), + // Generally, material cards do not have a highlight overlay. + highlightColor: Colors.transparent, + child: Semantics( + label: destination.title, + child: TravelDestinationContent(destination: destination), + ), + ), + ), + ), + ], + ), + ), + ); + } +} + +class SelectableTravelDestinationItem extends StatelessWidget { + const SelectableTravelDestinationItem({ + super.key, + required this.destination, + required this.isSelected, + required this.onSelected, + this.shape, + }); + + final TravelDestination destination; + final ShapeBorder? shape; + final bool isSelected; + final VoidCallback onSelected; + + // This height will allow for all the Card's content to fit comfortably within the card. + static const double height = 298.0; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + final String selectedStatus = isSelected + ? GalleryLocalizations.of(context)!.selected + : GalleryLocalizations.of(context)!.notSelected; + + return SafeArea( + top: false, + bottom: false, + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + SectionTitle(title: GalleryLocalizations.of(context)!.selectable), + SizedBox( + height: height, + child: Card( + // This ensures that the Card's children (including the ink splash) are clipped correctly. + clipBehavior: Clip.antiAlias, + shape: shape, + child: InkWell( + onLongPress: () { + onSelected(); + }, + // Generally, material cards use onSurface with 12% opacity for the pressed state. + splashColor: colorScheme.onSurface.withOpacity(0.12), + // Generally, material cards do not have a highlight overlay. + highlightColor: Colors.transparent, + child: Stack( + children: [ + Container( + color: isSelected + // Generally, material cards use primary with 8% opacity for the selected state. + // See: https://material.io/design/interaction/states.html#anatomy + ? colorScheme.primary.withOpacity(0.08) + : Colors.transparent, + ), + Semantics( + label: '${destination.title}, $selectedStatus', + onLongPressHint: isSelected + ? GalleryLocalizations.of(context)!.deselect + : GalleryLocalizations.of(context)!.select, + child: + TravelDestinationContent(destination: destination), + ), + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.all(8), + child: Icon( + Icons.check_circle, + color: isSelected + ? colorScheme.primary + : Colors.transparent, + ), + ), + ), + ], + ), + //), + ), + ), + ), + ], + ), + ), + ); + } +} + +class SectionTitle extends StatelessWidget { + const SectionTitle({ + super.key, + required this.title, + }); + + final String title; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 12), + child: Align( + alignment: Alignment.centerLeft, + child: Text(title, style: Theme.of(context).textTheme.titleMedium), + ), + ); + } +} + +class TravelDestinationContent extends StatelessWidget { + const TravelDestinationContent({super.key, required this.destination}); + + final TravelDestination destination; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final TextStyle titleStyle = theme.textTheme.headlineSmall!.copyWith( + color: Colors.white, + ); + final TextStyle descriptionStyle = theme.textTheme.titleMedium!; + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 184, + child: Stack( + children: [ + Positioned.fill( + // In order to have the ink splash appear above the image, you + // must use Ink.image. This allows the image to be painted as + // part of the Material and display ink effects above it. Using + // a standard Image will obscure the ink splash. + child: Ink.image( + image: AssetImage( + destination.assetName, + package: destination.assetPackage, + ), + fit: BoxFit.cover, + child: Container(), + ), + ), + Positioned( + bottom: 16, + left: 16, + right: 16, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Semantics( + container: true, + header: true, + child: Text( + destination.title, + style: titleStyle, + ), + ), + ), + ), + ], + ), + ), + // Description and share/explore buttons. + Semantics( + container: true, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: DefaultTextStyle( + softWrap: false, + overflow: TextOverflow.ellipsis, + style: descriptionStyle, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // This array contains the three line description on each card + // demo. + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( + destination.description, + style: descriptionStyle.copyWith(color: Colors.black54), + ), + ), + Text(destination.city), + Text(destination.location), + ], + ), + ), + ), + ), + if (destination.cardType == CardType.standard) + // share, explore buttons + Padding( + padding: const EdgeInsets.all(8), + child: OverflowBar( + alignment: MainAxisAlignment.start, + spacing: 8, + children: [ + TextButton( + onPressed: () {}, + child: Text(localizations.demoMenuShare, + semanticsLabel: localizations + .cardsDemoShareSemantics(destination.title)), + ), + TextButton( + onPressed: () {}, + child: Text(localizations.cardsDemoExplore, + semanticsLabel: localizations + .cardsDemoExploreSemantics(destination.title)), + ), + ], + ), + ), + ], + ); + } +} + +class CardsDemo extends StatefulWidget { + const CardsDemo({super.key}); + + @override + State createState() => _CardsDemoState(); +} + +class _CardsDemoState extends State with RestorationMixin { + final RestorableBool _isSelected = RestorableBool(false); + + @override + String get restorationId => 'cards_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_isSelected, 'is_selected'); + } + + @override + void dispose() { + _isSelected.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(GalleryLocalizations.of(context)!.demoCardTitle), + ), + body: Scrollbar( + child: ListView( + restorationId: 'cards_demo_list_view', + padding: const EdgeInsets.only(top: 8, left: 8, right: 8), + children: [ + for (final TravelDestination destination in destinations(context)) + Container( + margin: const EdgeInsets.only(bottom: 8), + child: (destination.cardType == CardType.standard) + ? TravelDestinationItem(destination: destination) + : destination.cardType == CardType.tappable + ? TappableTravelDestinationItem( + destination: destination) + : SelectableTravelDestinationItem( + destination: destination, + isSelected: _isSelected.value, + onSelected: () { + setState(() { + _isSelected.value = !_isSelected.value; + }); + }, + ), + ), + ], + ), + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/material/chip_demo.dart b/dev/integration_tests/new_gallery/lib/demos/material/chip_demo.dart new file mode 100644 index 0000000000..110b5e143c --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/chip_demo.dart @@ -0,0 +1,306 @@ +// 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:flutter/material.dart'; +import '../../gallery_localizations.dart'; +import 'material_demo_types.dart'; + +class ChipDemo extends StatelessWidget { + const ChipDemo({ + super.key, + required this.type, + }); + + final ChipDemoType type; + + String _title(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + switch (type) { + case ChipDemoType.action: + return localizations.demoActionChipTitle; + case ChipDemoType.choice: + return localizations.demoChoiceChipTitle; + case ChipDemoType.filter: + return localizations.demoFilterChipTitle; + case ChipDemoType.input: + return localizations.demoInputChipTitle; + } + } + + @override + Widget build(BuildContext context) { + Widget? buttons; + switch (type) { + case ChipDemoType.action: + buttons = _ActionChipDemo(); + case ChipDemoType.choice: + buttons = _ChoiceChipDemo(); + case ChipDemoType.filter: + buttons = _FilterChipDemo(); + case ChipDemoType.input: + buttons = _InputChipDemo(); + } + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(_title(context)), + ), + body: buttons, + ); + } +} + +// BEGIN chipDemoAction + +class _ActionChipDemo extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Center( + child: ActionChip( + onPressed: () {}, + avatar: const Icon( + Icons.brightness_5, + color: Colors.black54, + ), + label: Text(GalleryLocalizations.of(context)!.chipTurnOnLights), + ), + ); + } +} + +// END + +// BEGIN chipDemoChoice + +class _ChoiceChipDemo extends StatefulWidget { + @override + _ChoiceChipDemoState createState() => _ChoiceChipDemoState(); +} + +class _ChoiceChipDemoState extends State<_ChoiceChipDemo> + with RestorationMixin { + final RestorableIntN _indexSelected = RestorableIntN(null); + + @override + String get restorationId => 'choice_chip_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_indexSelected, 'choice_chip'); + } + + @override + void dispose() { + _indexSelected.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Wrap( + children: [ + ChoiceChip( + label: Text(localizations.chipSmall), + selected: _indexSelected.value == 0, + onSelected: (bool value) { + setState(() { + _indexSelected.value = value ? 0 : -1; + }); + }, + ), + const SizedBox(width: 8), + ChoiceChip( + label: Text(localizations.chipMedium), + selected: _indexSelected.value == 1, + onSelected: (bool value) { + setState(() { + _indexSelected.value = value ? 1 : -1; + }); + }, + ), + const SizedBox(width: 8), + ChoiceChip( + label: Text(localizations.chipLarge), + selected: _indexSelected.value == 2, + onSelected: (bool value) { + setState(() { + _indexSelected.value = value ? 2 : -1; + }); + }, + ), + ], + ), + const SizedBox(height: 12), + // Disabled chips + Wrap( + children: [ + ChoiceChip( + label: Text(localizations.chipSmall), + selected: _indexSelected.value == 0, + ), + const SizedBox(width: 8), + ChoiceChip( + label: Text(localizations.chipMedium), + selected: _indexSelected.value == 1, + ), + const SizedBox(width: 8), + ChoiceChip( + label: Text(localizations.chipLarge), + selected: _indexSelected.value == 2, + ), + ], + ), + ], + ), + ); + } +} + +// END + +// BEGIN chipDemoFilter + +class _FilterChipDemo extends StatefulWidget { + @override + _FilterChipDemoState createState() => _FilterChipDemoState(); +} + +class _FilterChipDemoState extends State<_FilterChipDemo> + with RestorationMixin { + final RestorableBool isSelectedElevator = RestorableBool(false); + final RestorableBool isSelectedWasher = RestorableBool(false); + final RestorableBool isSelectedFireplace = RestorableBool(false); + + @override + String get restorationId => 'filter_chip_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(isSelectedElevator, 'selected_elevator'); + registerForRestoration(isSelectedWasher, 'selected_washer'); + registerForRestoration(isSelectedFireplace, 'selected_fireplace'); + } + + @override + void dispose() { + isSelectedElevator.dispose(); + isSelectedWasher.dispose(); + isSelectedFireplace.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Wrap( + spacing: 8.0, + children: [ + FilterChip( + label: Text(localizations.chipElevator), + selected: isSelectedElevator.value, + onSelected: (bool value) { + setState(() { + isSelectedElevator.value = !isSelectedElevator.value; + }); + }, + ), + FilterChip( + label: Text(localizations.chipWasher), + selected: isSelectedWasher.value, + onSelected: (bool value) { + setState(() { + isSelectedWasher.value = !isSelectedWasher.value; + }); + }, + ), + FilterChip( + label: Text(localizations.chipFireplace), + selected: isSelectedFireplace.value, + onSelected: (bool value) { + setState(() { + isSelectedFireplace.value = !isSelectedFireplace.value; + }); + }, + ), + ], + ), + const SizedBox(height: 12), + // Disabled chips + Wrap( + spacing: 8.0, + children: [ + FilterChip( + label: Text(localizations.chipElevator), + selected: isSelectedElevator.value, + onSelected: null, + ), + FilterChip( + label: Text(localizations.chipWasher), + selected: isSelectedWasher.value, + onSelected: null, + ), + FilterChip( + label: Text(localizations.chipFireplace), + selected: isSelectedFireplace.value, + onSelected: null, + ), + ], + ), + ], + ), + ); + } +} + +// END + +// BEGIN chipDemoInput + +class _InputChipDemo extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + InputChip( + onPressed: () {}, + onDeleted: () {}, + avatar: const Icon( + Icons.directions_bike, + size: 20, + color: Colors.black54, + ), + deleteIconColor: Colors.black54, + label: Text(GalleryLocalizations.of(context)!.chipBiking), + ), + const SizedBox(height: 12), + // Disabled chip + InputChip( + avatar: const Icon( + Icons.directions_bike, + size: 20, + color: Colors.black54, + ), + deleteIconColor: Colors.black54, + label: Text(GalleryLocalizations.of(context)!.chipBiking), + ), + ], + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/material/data_table_demo.dart b/dev/integration_tests/new_gallery/lib/demos/material/data_table_demo.dart new file mode 100644 index 0000000000..d88ca630c4 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/data_table_demo.dart @@ -0,0 +1,680 @@ +// 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:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import '../../data/gallery_options.dart'; +import '../../gallery_localizations.dart'; + +// BEGIN dataTableDemo + +class DataTableDemo extends StatefulWidget { + const DataTableDemo({super.key}); + + @override + State createState() => _DataTableDemoState(); +} + +class _RestorableDessertSelections extends RestorableProperty> { + Set _dessertSelections = {}; + + /// Returns whether or not a dessert row is selected by index. + bool isSelected(int index) => _dessertSelections.contains(index); + + /// Takes a list of [_Dessert]s and saves the row indices of selected rows + /// into a [Set]. + void setDessertSelections(List<_Dessert> desserts) { + final Set updatedSet = {}; + for (int i = 0; i < desserts.length; i += 1) { + final _Dessert dessert = desserts[i]; + if (dessert.selected) { + updatedSet.add(i); + } + } + _dessertSelections = updatedSet; + notifyListeners(); + } + + @override + Set createDefaultValue() => _dessertSelections; + + @override + Set fromPrimitives(Object? data) { + final List selectedItemIndices = data! as List; + _dessertSelections = { + ...selectedItemIndices.map((dynamic id) => id as int), + }; + return _dessertSelections; + } + + @override + void initWithValue(Set value) { + _dessertSelections = value; + } + + @override + Object toPrimitives() => _dessertSelections.toList(); +} + +class _DataTableDemoState extends State with RestorationMixin { + final _RestorableDessertSelections _dessertSelections = + _RestorableDessertSelections(); + final RestorableInt _rowIndex = RestorableInt(0); + final RestorableInt _rowsPerPage = + RestorableInt(PaginatedDataTable.defaultRowsPerPage); + final RestorableBool _sortAscending = RestorableBool(true); + final RestorableIntN _sortColumnIndex = RestorableIntN(null); + _DessertDataSource? _dessertsDataSource; + + @override + String get restorationId => 'data_table_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_dessertSelections, 'selected_row_indices'); + registerForRestoration(_rowIndex, 'current_row_index'); + registerForRestoration(_rowsPerPage, 'rows_per_page'); + registerForRestoration(_sortAscending, 'sort_ascending'); + registerForRestoration(_sortColumnIndex, 'sort_column_index'); + + _dessertsDataSource ??= _DessertDataSource(context); + switch (_sortColumnIndex.value) { + case 0: + _dessertsDataSource!._sort((_Dessert d) => d.name, _sortAscending.value); + case 1: + _dessertsDataSource! + ._sort((_Dessert d) => d.calories, _sortAscending.value); + case 2: + _dessertsDataSource!._sort((_Dessert d) => d.fat, _sortAscending.value); + case 3: + _dessertsDataSource!._sort((_Dessert d) => d.carbs, _sortAscending.value); + case 4: + _dessertsDataSource!._sort((_Dessert d) => d.protein, _sortAscending.value); + case 5: + _dessertsDataSource!._sort((_Dessert d) => d.sodium, _sortAscending.value); + case 6: + _dessertsDataSource!._sort((_Dessert d) => d.calcium, _sortAscending.value); + case 7: + _dessertsDataSource!._sort((_Dessert d) => d.iron, _sortAscending.value); + } + _dessertsDataSource!.updateSelectedDesserts(_dessertSelections); + _dessertsDataSource!.addListener(_updateSelectedDessertRowListener); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _dessertsDataSource ??= _DessertDataSource(context); + _dessertsDataSource!.addListener(_updateSelectedDessertRowListener); + } + + void _updateSelectedDessertRowListener() { + _dessertSelections.setDessertSelections(_dessertsDataSource!._desserts); + } + + void _sort( + Comparable Function(_Dessert d) getField, + int columnIndex, + bool ascending, + ) { + _dessertsDataSource!._sort(getField, ascending); + setState(() { + _sortColumnIndex.value = columnIndex; + _sortAscending.value = ascending; + }); + } + + @override + void dispose() { + _rowsPerPage.dispose(); + _sortColumnIndex.dispose(); + _sortAscending.dispose(); + _dessertsDataSource!.removeListener(_updateSelectedDessertRowListener); + _dessertsDataSource!.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(localizations.demoDataTableTitle), + ), + body: Scrollbar( + child: ListView( + restorationId: 'data_table_list_view', + padding: const EdgeInsets.all(16), + children: [ + PaginatedDataTable( + header: Text(localizations.dataTableHeader), + rowsPerPage: _rowsPerPage.value, + onRowsPerPageChanged: (int? value) { + setState(() { + _rowsPerPage.value = value!; + }); + }, + initialFirstRowIndex: _rowIndex.value, + onPageChanged: (int rowIndex) { + setState(() { + _rowIndex.value = rowIndex; + }); + }, + sortColumnIndex: _sortColumnIndex.value, + sortAscending: _sortAscending.value, + onSelectAll: _dessertsDataSource!._selectAll, + columns: [ + DataColumn( + label: Text(localizations.dataTableColumnDessert), + onSort: (int columnIndex, bool ascending) => + _sort((_Dessert d) => d.name, columnIndex, ascending), + ), + DataColumn( + label: Text(localizations.dataTableColumnCalories), + numeric: true, + onSort: (int columnIndex, bool ascending) => + _sort((_Dessert d) => d.calories, columnIndex, ascending), + ), + DataColumn( + label: Text(localizations.dataTableColumnFat), + numeric: true, + onSort: (int columnIndex, bool ascending) => + _sort((_Dessert d) => d.fat, columnIndex, ascending), + ), + DataColumn( + label: Text(localizations.dataTableColumnCarbs), + numeric: true, + onSort: (int columnIndex, bool ascending) => + _sort((_Dessert d) => d.carbs, columnIndex, ascending), + ), + DataColumn( + label: Text(localizations.dataTableColumnProtein), + numeric: true, + onSort: (int columnIndex, bool ascending) => + _sort((_Dessert d) => d.protein, columnIndex, ascending), + ), + DataColumn( + label: Text(localizations.dataTableColumnSodium), + numeric: true, + onSort: (int columnIndex, bool ascending) => + _sort((_Dessert d) => d.sodium, columnIndex, ascending), + ), + DataColumn( + label: Text(localizations.dataTableColumnCalcium), + numeric: true, + onSort: (int columnIndex, bool ascending) => + _sort((_Dessert d) => d.calcium, columnIndex, ascending), + ), + DataColumn( + label: Text(localizations.dataTableColumnIron), + numeric: true, + onSort: (int columnIndex, bool ascending) => + _sort((_Dessert d) => d.iron, columnIndex, ascending), + ), + ], + source: _dessertsDataSource!, + ), + ], + ), + ), + ); + } +} + +class _Dessert { + _Dessert( + this.name, + this.calories, + this.fat, + this.carbs, + this.protein, + this.sodium, + this.calcium, + this.iron, + ); + + final String name; + final int calories; + final double fat; + final int carbs; + final double protein; + final int sodium; + final int calcium; + final int iron; + bool selected = false; +} + +class _DessertDataSource extends DataTableSource { + _DessertDataSource(this.context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + _desserts = <_Dessert>[ + _Dessert( + localizations.dataTableRowFrozenYogurt, + 159, + 6.0, + 24, + 4.0, + 87, + 14, + 1, + ), + _Dessert( + localizations.dataTableRowIceCreamSandwich, + 237, + 9.0, + 37, + 4.3, + 129, + 8, + 1, + ), + _Dessert( + localizations.dataTableRowEclair, + 262, + 16.0, + 24, + 6.0, + 337, + 6, + 7, + ), + _Dessert( + localizations.dataTableRowCupcake, + 305, + 3.7, + 67, + 4.3, + 413, + 3, + 8, + ), + _Dessert( + localizations.dataTableRowGingerbread, + 356, + 16.0, + 49, + 3.9, + 327, + 7, + 16, + ), + _Dessert( + localizations.dataTableRowJellyBean, + 375, + 0.0, + 94, + 0.0, + 50, + 0, + 0, + ), + _Dessert( + localizations.dataTableRowLollipop, + 392, + 0.2, + 98, + 0.0, + 38, + 0, + 2, + ), + _Dessert( + localizations.dataTableRowHoneycomb, + 408, + 3.2, + 87, + 6.5, + 562, + 0, + 45, + ), + _Dessert( + localizations.dataTableRowDonut, + 452, + 25.0, + 51, + 4.9, + 326, + 2, + 22, + ), + _Dessert( + localizations.dataTableRowApplePie, + 518, + 26.0, + 65, + 7.0, + 54, + 12, + 6, + ), + _Dessert( + localizations.dataTableRowWithSugar( + localizations.dataTableRowFrozenYogurt, + ), + 168, + 6.0, + 26, + 4.0, + 87, + 14, + 1, + ), + _Dessert( + localizations.dataTableRowWithSugar( + localizations.dataTableRowIceCreamSandwich, + ), + 246, + 9.0, + 39, + 4.3, + 129, + 8, + 1, + ), + _Dessert( + localizations.dataTableRowWithSugar( + localizations.dataTableRowEclair, + ), + 271, + 16.0, + 26, + 6.0, + 337, + 6, + 7, + ), + _Dessert( + localizations.dataTableRowWithSugar( + localizations.dataTableRowCupcake, + ), + 314, + 3.7, + 69, + 4.3, + 413, + 3, + 8, + ), + _Dessert( + localizations.dataTableRowWithSugar( + localizations.dataTableRowGingerbread, + ), + 345, + 16.0, + 51, + 3.9, + 327, + 7, + 16, + ), + _Dessert( + localizations.dataTableRowWithSugar( + localizations.dataTableRowJellyBean, + ), + 364, + 0.0, + 96, + 0.0, + 50, + 0, + 0, + ), + _Dessert( + localizations.dataTableRowWithSugar( + localizations.dataTableRowLollipop, + ), + 401, + 0.2, + 100, + 0.0, + 38, + 0, + 2, + ), + _Dessert( + localizations.dataTableRowWithSugar( + localizations.dataTableRowHoneycomb, + ), + 417, + 3.2, + 89, + 6.5, + 562, + 0, + 45, + ), + _Dessert( + localizations.dataTableRowWithSugar( + localizations.dataTableRowDonut, + ), + 461, + 25.0, + 53, + 4.9, + 326, + 2, + 22, + ), + _Dessert( + localizations.dataTableRowWithSugar( + localizations.dataTableRowApplePie, + ), + 527, + 26.0, + 67, + 7.0, + 54, + 12, + 6, + ), + _Dessert( + localizations.dataTableRowWithHoney( + localizations.dataTableRowFrozenYogurt, + ), + 223, + 6.0, + 36, + 4.0, + 87, + 14, + 1, + ), + _Dessert( + localizations.dataTableRowWithHoney( + localizations.dataTableRowIceCreamSandwich, + ), + 301, + 9.0, + 49, + 4.3, + 129, + 8, + 1, + ), + _Dessert( + localizations.dataTableRowWithHoney( + localizations.dataTableRowEclair, + ), + 326, + 16.0, + 36, + 6.0, + 337, + 6, + 7, + ), + _Dessert( + localizations.dataTableRowWithHoney( + localizations.dataTableRowCupcake, + ), + 369, + 3.7, + 79, + 4.3, + 413, + 3, + 8, + ), + _Dessert( + localizations.dataTableRowWithHoney( + localizations.dataTableRowGingerbread, + ), + 420, + 16.0, + 61, + 3.9, + 327, + 7, + 16, + ), + _Dessert( + localizations.dataTableRowWithHoney( + localizations.dataTableRowJellyBean, + ), + 439, + 0.0, + 106, + 0.0, + 50, + 0, + 0, + ), + _Dessert( + localizations.dataTableRowWithHoney( + localizations.dataTableRowLollipop, + ), + 456, + 0.2, + 110, + 0.0, + 38, + 0, + 2, + ), + _Dessert( + localizations.dataTableRowWithHoney( + localizations.dataTableRowHoneycomb, + ), + 472, + 3.2, + 99, + 6.5, + 562, + 0, + 45, + ), + _Dessert( + localizations.dataTableRowWithHoney( + localizations.dataTableRowDonut, + ), + 516, + 25.0, + 63, + 4.9, + 326, + 2, + 22, + ), + _Dessert( + localizations.dataTableRowWithHoney( + localizations.dataTableRowApplePie, + ), + 582, + 26.0, + 77, + 7.0, + 54, + 12, + 6, + ), + ]; + } + + final BuildContext context; + late List<_Dessert> _desserts; + + void _sort(Comparable Function(_Dessert d) getField, bool ascending) { + _desserts.sort((_Dessert a, _Dessert b) { + final Comparable aValue = getField(a); + final Comparable bValue = getField(b); + return ascending + ? Comparable.compare(aValue, bValue) + : Comparable.compare(bValue, aValue); + }); + notifyListeners(); + } + + int _selectedCount = 0; + + void updateSelectedDesserts(_RestorableDessertSelections selectedRows) { + _selectedCount = 0; + for (int i = 0; i < _desserts.length; i += 1) { + final _Dessert dessert = _desserts[i]; + if (selectedRows.isSelected(i)) { + dessert.selected = true; + _selectedCount += 1; + } else { + dessert.selected = false; + } + } + notifyListeners(); + } + + @override + DataRow? getRow(int index) { + final NumberFormat format = NumberFormat.decimalPercentPattern( + locale: GalleryOptions.of(context).locale.toString(), + decimalDigits: 0, + ); + assert(index >= 0); + if (index >= _desserts.length) { + return null; + } + final _Dessert dessert = _desserts[index]; + return DataRow.byIndex( + index: index, + selected: dessert.selected, + onSelectChanged: (bool? value) { + if (dessert.selected != value) { + _selectedCount += value! ? 1 : -1; + assert(_selectedCount >= 0); + dessert.selected = value; + notifyListeners(); + } + }, + cells: [ + DataCell(Text(dessert.name)), + DataCell(Text('${dessert.calories}')), + DataCell(Text(dessert.fat.toStringAsFixed(1))), + DataCell(Text('${dessert.carbs}')), + DataCell(Text(dessert.protein.toStringAsFixed(1))), + DataCell(Text('${dessert.sodium}')), + DataCell(Text(format.format(dessert.calcium / 100))), + DataCell(Text(format.format(dessert.iron / 100))), + ], + ); + } + + @override + int get rowCount => _desserts.length; + + @override + bool get isRowCountApproximate => false; + + @override + int get selectedRowCount => _selectedCount; + + void _selectAll(bool? checked) { + for (final _Dessert dessert in _desserts) { + dessert.selected = checked ?? false; + } + _selectedCount = checked! ? _desserts.length : 0; + notifyListeners(); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/material/dialog_demo.dart b/dev/integration_tests/new_gallery/lib/demos/material/dialog_demo.dart new file mode 100644 index 0000000000..e9cb3c3fc2 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/dialog_demo.dart @@ -0,0 +1,344 @@ +// 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:flutter/material.dart'; + +import '../../data/gallery_options.dart'; +import '../../gallery_localizations.dart'; +import 'material_demo_types.dart'; + +// BEGIN dialogDemo + +class DialogDemo extends StatefulWidget { + const DialogDemo({super.key, required this.type}); + + final DialogDemoType type; + + @override + State createState() => _DialogDemoState(); +} + +class _DialogDemoState extends State with RestorationMixin { + late RestorableRouteFuture _alertDialogRoute; + late RestorableRouteFuture _alertDialogWithTitleRoute; + late RestorableRouteFuture _simpleDialogRoute; + + @override + String get restorationId => 'dialog_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration( + _alertDialogRoute, + 'alert_demo_dialog_route', + ); + registerForRestoration( + _alertDialogWithTitleRoute, + 'alert_demo_with_title_dialog_route', + ); + registerForRestoration( + _simpleDialogRoute, + 'simple_dialog_route', + ); + } + + // Displays the popped String value in a SnackBar. + void _showInSnackBar(String value) { + // The value passed to Navigator.pop() or null. + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + GalleryLocalizations.of(context)!.dialogSelectedOption(value), + ), + ), + ); + } + + @override + void initState() { + super.initState(); + _alertDialogRoute = RestorableRouteFuture( + onPresent: (NavigatorState navigator, Object? arguments) { + return navigator.restorablePush(_alertDialogDemoRoute); + }, + onComplete: _showInSnackBar, + ); + _alertDialogWithTitleRoute = RestorableRouteFuture( + onPresent: (NavigatorState navigator, Object? arguments) { + return navigator.restorablePush(_alertDialogWithTitleDemoRoute); + }, + onComplete: _showInSnackBar, + ); + _simpleDialogRoute = RestorableRouteFuture( + onPresent: (NavigatorState navigator, Object? arguments) { + return navigator.restorablePush(_simpleDialogDemoRoute); + }, + onComplete: _showInSnackBar, + ); + } + + String _title(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + switch (widget.type) { + case DialogDemoType.alert: + return localizations.demoAlertDialogTitle; + case DialogDemoType.alertTitle: + return localizations.demoAlertTitleDialogTitle; + case DialogDemoType.simple: + return localizations.demoSimpleDialogTitle; + case DialogDemoType.fullscreen: + return localizations.demoFullscreenDialogTitle; + } + } + + static Route _alertDialogDemoRoute( + BuildContext context, + Object? arguments, + ) { + final ThemeData theme = Theme.of(context); + final TextStyle dialogTextStyle = theme.textTheme.titleMedium! + .copyWith(color: theme.textTheme.bodySmall!.color); + + return DialogRoute( + context: context, + builder: (BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return ApplyTextOptions( + child: AlertDialog( + content: Text( + localizations.dialogDiscardTitle, + style: dialogTextStyle, + ), + actions: [ + _DialogButton(text: localizations.dialogCancel), + _DialogButton(text: localizations.dialogDiscard), + ], + )); + }, + ); + } + + static Route _alertDialogWithTitleDemoRoute( + BuildContext context, + Object? arguments, + ) { + final ThemeData theme = Theme.of(context); + final TextStyle dialogTextStyle = theme.textTheme.titleMedium! + .copyWith(color: theme.textTheme.bodySmall!.color); + + return DialogRoute( + context: context, + builder: (BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return ApplyTextOptions( + child: AlertDialog( + title: Text(localizations.dialogLocationTitle), + content: Text( + localizations.dialogLocationDescription, + style: dialogTextStyle, + ), + actions: [ + _DialogButton(text: localizations.dialogDisagree), + _DialogButton(text: localizations.dialogAgree), + ], + ), + ); + }, + ); + } + + static Route _simpleDialogDemoRoute( + BuildContext context, + Object? arguments, + ) { + final ThemeData theme = Theme.of(context); + + return DialogRoute( + context: context, + builder: (BuildContext context) => ApplyTextOptions( + child: SimpleDialog( + title: Text(GalleryLocalizations.of(context)!.dialogSetBackup), + children: [ + _DialogDemoItem( + icon: Icons.account_circle, + color: theme.colorScheme.primary, + text: 'username@gmail.com', + ), + _DialogDemoItem( + icon: Icons.account_circle, + color: theme.colorScheme.secondary, + text: 'user02@gmail.com', + ), + _DialogDemoItem( + icon: Icons.add_circle, + text: GalleryLocalizations.of(context)!.dialogAddAccount, + color: theme.disabledColor, + ), + ], + ), + ), + ); + } + + static Route _fullscreenDialogRoute( + BuildContext context, + Object? arguments, + ) { + return MaterialPageRoute( + builder: (BuildContext context) => _FullScreenDialogDemo(), + fullscreenDialog: true, + ); + } + + @override + Widget build(BuildContext context) { + return Navigator( + // Adding [ValueKey] to make sure that the widget gets rebuilt when + // changing type. + key: ValueKey(widget.type), + restorationScopeId: 'navigator', + onGenerateRoute: (RouteSettings settings) { + return _NoAnimationMaterialPageRoute( + settings: settings, + builder: (BuildContext context) => Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(_title(context)), + ), + body: Center( + child: ElevatedButton( + onPressed: () { + switch (widget.type) { + case DialogDemoType.alert: + _alertDialogRoute.present(); + case DialogDemoType.alertTitle: + _alertDialogWithTitleRoute.present(); + case DialogDemoType.simple: + _simpleDialogRoute.present(); + case DialogDemoType.fullscreen: + Navigator.restorablePush( + context, _fullscreenDialogRoute); + } + }, + child: Text(GalleryLocalizations.of(context)!.dialogShow), + ), + ), + ), + ); + }, + ); + } +} + +/// A MaterialPageRoute without any transition animations. +class _NoAnimationMaterialPageRoute extends MaterialPageRoute { + _NoAnimationMaterialPageRoute({ + required super.builder, + super.settings, + super.maintainState, + super.fullscreenDialog, + }); + + @override + Widget buildTransitions( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return child; + } +} + +class _DialogButton extends StatelessWidget { + const _DialogButton({required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return TextButton( + onPressed: () { + Navigator.of(context).pop(text); + }, + child: Text(text), + ); + } +} + +class _DialogDemoItem extends StatelessWidget { + const _DialogDemoItem({ + this.icon, + this.color, + required this.text, + }); + + final IconData? icon; + final Color? color; + final String text; + + @override + Widget build(BuildContext context) { + return SimpleDialogOption( + onPressed: () { + Navigator.of(context).pop(text); + }, + child: Row( + children: [ + Icon(icon, size: 36, color: color), + Flexible( + child: Padding( + padding: const EdgeInsetsDirectional.only(start: 16), + child: Text(text), + ), + ), + ], + ), + ); + } +} + +class _FullScreenDialogDemo extends StatelessWidget { + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + // Remove the MediaQuery padding because the demo is rendered inside of a + // different page that already accounts for this padding. + return MediaQuery.removePadding( + context: context, + removeTop: true, + removeBottom: true, + child: ApplyTextOptions( + child: Scaffold( + appBar: AppBar( + title: Text(localizations.dialogFullscreenTitle), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: Text( + localizations.dialogFullscreenSave, + style: theme.textTheme.bodyMedium!.copyWith( + color: theme.colorScheme.onPrimary, + ), + ), + ), + ], + ), + body: Center( + child: Text( + localizations.dialogFullscreenDescription, + ), + ), + ), + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/material/divider_demo.dart b/dev/integration_tests/new_gallery/lib/demos/material/divider_demo.dart new file mode 100644 index 0000000000..1fc13d23ab --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/divider_demo.dart @@ -0,0 +1,123 @@ +// 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:flutter/material.dart'; +import '../../gallery_localizations.dart'; +import 'material_demo_types.dart'; + +class DividerDemo extends StatelessWidget { + const DividerDemo({super.key, required this.type}); + + final DividerDemoType type; + + String _title(BuildContext context) { + switch (type) { + case DividerDemoType.horizontal: + return GalleryLocalizations.of(context)!.demoDividerTitle; + case DividerDemoType.vertical: + return GalleryLocalizations.of(context)!.demoVerticalDividerTitle; + } + } + + @override + Widget build(BuildContext context) { + late Widget dividers; + switch (type) { + case DividerDemoType.horizontal: + dividers = _HorizontalDividerDemo(); + case DividerDemoType.vertical: + dividers = _VerticalDividerDemo(); + } + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text( + _title(context), + ), + ), + body: dividers, + ); + } +} + +// BEGIN dividerDemo + +class _HorizontalDividerDemo extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(10), + child: Column( + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.deepPurpleAccent, + ), + ), + ), + const Divider( + color: Colors.grey, + height: 20, + thickness: 1, + indent: 20, + endIndent: 0, + ), + Expanded( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.deepOrangeAccent, + ), + ), + ), + ], + ), + ); + } +} + +// END + +// BEGIN verticalDividerDemo + +class _VerticalDividerDemo extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(10), + child: Row( + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.deepPurpleAccent, + ), + ), + ), + const VerticalDivider( + color: Colors.grey, + thickness: 1, + indent: 20, + endIndent: 0, + width: 20, + ), + Expanded( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.deepOrangeAccent, + ), + ), + ), + ], + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/material/grid_list_demo.dart b/dev/integration_tests/new_gallery/lib/demos/material/grid_list_demo.dart new file mode 100644 index 0000000000..81951e79b5 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/grid_list_demo.dart @@ -0,0 +1,196 @@ +// 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:flutter/material.dart'; +import '../../gallery_localizations.dart'; +import 'material_demo_types.dart'; + +// BEGIN gridListsDemo + +class GridListDemo extends StatelessWidget { + const GridListDemo({super.key, required this.type}); + + final GridListDemoType type; + + List<_Photo> _photos(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return <_Photo>[ + _Photo( + assetName: 'places/india_chennai_flower_market.png', + title: localizations.placeChennai, + subtitle: localizations.placeFlowerMarket, + ), + _Photo( + assetName: 'places/india_tanjore_bronze_works.png', + title: localizations.placeTanjore, + subtitle: localizations.placeBronzeWorks, + ), + _Photo( + assetName: 'places/india_tanjore_market_merchant.png', + title: localizations.placeTanjore, + subtitle: localizations.placeMarket, + ), + _Photo( + assetName: 'places/india_tanjore_thanjavur_temple.png', + title: localizations.placeTanjore, + subtitle: localizations.placeThanjavurTemple, + ), + _Photo( + assetName: 'places/india_tanjore_thanjavur_temple_carvings.png', + title: localizations.placeTanjore, + subtitle: localizations.placeThanjavurTemple, + ), + _Photo( + assetName: 'places/india_pondicherry_salt_farm.png', + title: localizations.placePondicherry, + subtitle: localizations.placeSaltFarm, + ), + _Photo( + assetName: 'places/india_chennai_highway.png', + title: localizations.placeChennai, + subtitle: localizations.placeScooters, + ), + _Photo( + assetName: 'places/india_chettinad_silk_maker.png', + title: localizations.placeChettinad, + subtitle: localizations.placeSilkMaker, + ), + _Photo( + assetName: 'places/india_chettinad_produce.png', + title: localizations.placeChettinad, + subtitle: localizations.placeLunchPrep, + ), + _Photo( + assetName: 'places/india_tanjore_market_technology.png', + title: localizations.placeTanjore, + subtitle: localizations.placeMarket, + ), + _Photo( + assetName: 'places/india_pondicherry_beach.png', + title: localizations.placePondicherry, + subtitle: localizations.placeBeach, + ), + _Photo( + assetName: 'places/india_pondicherry_fisherman.png', + title: localizations.placePondicherry, + subtitle: localizations.placeFisherman, + ), + ]; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(GalleryLocalizations.of(context)!.demoGridListsTitle), + ), + body: GridView.count( + restorationId: 'grid_view_demo_grid_offset', + crossAxisCount: 2, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + padding: const EdgeInsets.all(8), + children: _photos(context).map((_Photo photo) { + return _GridDemoPhotoItem( + photo: photo, + tileStyle: type, + ); + }).toList(), + ), + ); + } +} + +class _Photo { + _Photo({ + required this.assetName, + required this.title, + required this.subtitle, + }); + + final String assetName; + final String title; + final String subtitle; +} + +/// Allow the text size to shrink to fit in the space +class _GridTitleText extends StatelessWidget { + const _GridTitleText(this.text); + + final String text; + + @override + Widget build(BuildContext context) { + return FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerStart, + child: Text(text), + ); + } +} + +class _GridDemoPhotoItem extends StatelessWidget { + const _GridDemoPhotoItem({ + required this.photo, + required this.tileStyle, + }); + + final _Photo photo; + final GridListDemoType tileStyle; + + @override + Widget build(BuildContext context) { + final Widget image = Semantics( + label: '${photo.title} ${photo.subtitle}', + child: Material( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + clipBehavior: Clip.antiAlias, + child: Image.asset( + photo.assetName, + package: 'flutter_gallery_assets', + fit: BoxFit.cover, + ), + ), + ); + + switch (tileStyle) { + case GridListDemoType.imageOnly: + return image; + case GridListDemoType.header: + return GridTile( + header: Material( + color: Colors.transparent, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(4)), + ), + clipBehavior: Clip.antiAlias, + child: GridTileBar( + title: _GridTitleText(photo.title), + backgroundColor: Colors.black45, + ), + ), + child: image, + ); + case GridListDemoType.footer: + return GridTile( + footer: Material( + color: Colors.transparent, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(bottom: Radius.circular(4)), + ), + clipBehavior: Clip.antiAlias, + child: GridTileBar( + backgroundColor: Colors.black45, + title: _GridTitleText(photo.title), + subtitle: _GridTitleText(photo.subtitle), + ), + ), + child: image, + ); + } + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/material/list_demo.dart b/dev/integration_tests/new_gallery/lib/demos/material/list_demo.dart new file mode 100644 index 0000000000..04c2b25f10 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/list_demo.dart @@ -0,0 +1,49 @@ +// 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:flutter/material.dart'; + +import '../../gallery_localizations.dart'; +import 'material_demo_types.dart'; + +// BEGIN listDemo + +class ListDemo extends StatelessWidget { + const ListDemo({super.key, required this.type}); + + final ListDemoType type; + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(localizations.demoListsTitle), + ), + body: Scrollbar( + child: ListView( + restorationId: 'list_demo_list_view', + padding: const EdgeInsets.symmetric(vertical: 8), + children: [ + for (int index = 1; index < 21; index++) + ListTile( + leading: ExcludeSemantics( + child: CircleAvatar(child: Text('$index')), + ), + title: Text( + localizations.demoBottomSheetItem(index), + ), + subtitle: type == ListDemoType.twoLine + ? Text(localizations.demoListsSecondary) + : null, + ), + ], + ), + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/material/material_demo_types.dart b/dev/integration_tests/new_gallery/lib/demos/material/material_demo_types.dart new file mode 100644 index 0000000000..dbe3449931 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/material_demo_types.dart @@ -0,0 +1,86 @@ +// 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. + +enum BottomNavigationDemoType { + withLabels, + withoutLabels, +} + +enum BottomSheetDemoType { + persistent, + modal, +} + +enum ButtonDemoType { + text, + elevated, + outlined, + toggle, + floating, +} + +enum ChipDemoType { + action, + choice, + filter, + input, +} + +enum DialogDemoType { + alert, + alertTitle, + simple, + fullscreen, +} + +enum GridListDemoType { + imageOnly, + header, + footer, +} + +enum ListDemoType { + oneLine, + twoLine, +} + +enum MenuDemoType { + contextMenu, + sectionedMenu, + simpleMenu, + checklistMenu, +} + +enum PickerDemoType { + date, + time, + range, +} + +enum ProgressIndicatorDemoType { + circular, + linear, +} + +enum SelectionControlsDemoType { + checkbox, + radio, + switches, +} + +enum SlidersDemoType { + sliders, + rangeSliders, + customSliders, +} + +enum TabsDemoType { + scrollable, + nonScrollable, +} + +enum DividerDemoType { + horizontal, + vertical, +} diff --git a/dev/integration_tests/new_gallery/lib/demos/material/material_demos.dart b/dev/integration_tests/new_gallery/lib/demos/material/material_demos.dart new file mode 100644 index 0000000000..bb73a82ffc --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/material_demos.dart @@ -0,0 +1,28 @@ +// 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. + +export 'package:gallery/demos/material/app_bar_demo.dart'; +export 'package:gallery/demos/material/banner_demo.dart'; +export 'package:gallery/demos/material/bottom_app_bar_demo.dart'; +export 'package:gallery/demos/material/bottom_navigation_demo.dart'; +export 'package:gallery/demos/material/bottom_sheet_demo.dart'; +export 'package:gallery/demos/material/button_demo.dart'; +export 'package:gallery/demos/material/cards_demo.dart'; +export 'package:gallery/demos/material/chip_demo.dart'; +export 'package:gallery/demos/material/data_table_demo.dart'; +export 'package:gallery/demos/material/dialog_demo.dart'; +export 'package:gallery/demos/material/divider_demo.dart'; +export 'package:gallery/demos/material/grid_list_demo.dart'; +export 'package:gallery/demos/material/list_demo.dart'; +export 'package:gallery/demos/material/menu_demo.dart'; +export 'package:gallery/demos/material/navigation_drawer.dart'; +export 'package:gallery/demos/material/navigation_rail_demo.dart'; +export 'package:gallery/demos/material/picker_demo.dart'; +export 'package:gallery/demos/material/progress_indicator_demo.dart'; +export 'package:gallery/demos/material/selection_controls_demo.dart'; +export 'package:gallery/demos/material/sliders_demo.dart'; +export 'package:gallery/demos/material/snackbar_demo.dart'; +export 'package:gallery/demos/material/tabs_demo.dart'; +export 'package:gallery/demos/material/text_field_demo.dart'; +export 'package:gallery/demos/material/tooltip_demo.dart'; diff --git a/dev/integration_tests/new_gallery/lib/demos/material/menu_demo.dart b/dev/integration_tests/new_gallery/lib/demos/material/menu_demo.dart new file mode 100644 index 0000000000..56d44f0942 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/menu_demo.dart @@ -0,0 +1,416 @@ +// 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:flutter/material.dart'; + +import '../../gallery_localizations.dart'; +import 'material_demo_types.dart'; + +enum SimpleValue { + one, + two, + three, +} + +enum CheckedValue { + one, + two, + three, + four, +} + +class MenuDemo extends StatefulWidget { + const MenuDemo({super.key, required this.type}); + + final MenuDemoType type; + + @override + State createState() => _MenuDemoState(); +} + +class _MenuDemoState extends State { + void showInSnackBar(String value) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(value), + )); + } + + @override + Widget build(BuildContext context) { + Widget demo; + switch (widget.type) { + case MenuDemoType.contextMenu: + demo = _ContextMenuDemo(showInSnackBar: showInSnackBar); + case MenuDemoType.sectionedMenu: + demo = _SectionedMenuDemo(showInSnackBar: showInSnackBar); + case MenuDemoType.simpleMenu: + demo = _SimpleMenuDemo(showInSnackBar: showInSnackBar); + case MenuDemoType.checklistMenu: + demo = _ChecklistMenuDemo(showInSnackBar: showInSnackBar); + } + + return Scaffold( + appBar: AppBar( + title: Text(GalleryLocalizations.of(context)!.demoMenuTitle), + automaticallyImplyLeading: false, + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Center( + child: demo, + ), + ), + ); + } +} + +// BEGIN menuDemoContext + +// Pressing the PopupMenuButton on the right of this item shows +// a simple menu with one disabled item. Typically the contents +// of this "contextual menu" would reflect the app's state. +class _ContextMenuDemo extends StatelessWidget { + const _ContextMenuDemo({required this.showInSnackBar}); + + final void Function(String value) showInSnackBar; + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return ListTile( + title: Text(localizations.demoMenuAnItemWithAContextMenuButton), + trailing: PopupMenuButton( + padding: EdgeInsets.zero, + onSelected: (String value) => showInSnackBar( + localizations.demoMenuSelected(value), + ), + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: localizations.demoMenuContextMenuItemOne, + child: Text( + localizations.demoMenuContextMenuItemOne, + ), + ), + PopupMenuItem( + enabled: false, + child: Text( + localizations.demoMenuADisabledMenuItem, + ), + ), + PopupMenuItem( + value: localizations.demoMenuContextMenuItemThree, + child: Text( + localizations.demoMenuContextMenuItemThree, + ), + ), + ], + ), + ); + } +} + +// END + +// BEGIN menuDemoSectioned + +// Pressing the PopupMenuButton on the right of this item shows +// a menu whose items have text labels and icons and a divider +// That separates the first three items from the last one. +class _SectionedMenuDemo extends StatelessWidget { + const _SectionedMenuDemo({required this.showInSnackBar}); + + final void Function(String value) showInSnackBar; + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return ListTile( + title: Text(localizations.demoMenuAnItemWithASectionedMenu), + trailing: PopupMenuButton( + padding: EdgeInsets.zero, + onSelected: (String value) => + showInSnackBar(localizations.demoMenuSelected(value)), + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: localizations.demoMenuPreview, + child: ListTile( + leading: const Icon(Icons.visibility), + title: Text( + localizations.demoMenuPreview, + ), + ), + ), + PopupMenuItem( + value: localizations.demoMenuShare, + child: ListTile( + leading: const Icon(Icons.person_add), + title: Text( + localizations.demoMenuShare, + ), + ), + ), + PopupMenuItem( + value: localizations.demoMenuGetLink, + child: ListTile( + leading: const Icon(Icons.link), + title: Text( + localizations.demoMenuGetLink, + ), + ), + ), + const PopupMenuDivider(), + PopupMenuItem( + value: localizations.demoMenuRemove, + child: ListTile( + leading: const Icon(Icons.delete), + title: Text( + localizations.demoMenuRemove, + ), + ), + ), + ], + ), + ); + } +} + +// END + +// BEGIN menuDemoSimple + +// This entire list item is a PopupMenuButton. Tapping anywhere shows +// a menu whose current value is highlighted and aligned over the +// list item's center line. +class _SimpleMenuDemo extends StatefulWidget { + const _SimpleMenuDemo({required this.showInSnackBar}); + + final void Function(String value) showInSnackBar; + + @override + _SimpleMenuDemoState createState() => _SimpleMenuDemoState(); +} + +class _SimpleMenuDemoState extends State<_SimpleMenuDemo> { + late SimpleValue _simpleValue; + + void showAndSetMenuSelection(BuildContext context, SimpleValue value) { + setState(() { + _simpleValue = value; + }); + widget.showInSnackBar( + GalleryLocalizations.of(context)! + .demoMenuSelected(simpleValueToString(context, value)), + ); + } + + String simpleValueToString(BuildContext context, SimpleValue value) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return { + SimpleValue.one: localizations.demoMenuItemValueOne, + SimpleValue.two: localizations.demoMenuItemValueTwo, + SimpleValue.three: localizations.demoMenuItemValueThree, + }[value]!; + } + + @override + void initState() { + super.initState(); + _simpleValue = SimpleValue.two; + } + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + padding: EdgeInsets.zero, + initialValue: _simpleValue, + onSelected: (SimpleValue value) => showAndSetMenuSelection(context, value), + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: SimpleValue.one, + child: Text(simpleValueToString( + context, + SimpleValue.one, + )), + ), + PopupMenuItem( + value: SimpleValue.two, + child: Text(simpleValueToString( + context, + SimpleValue.two, + )), + ), + PopupMenuItem( + value: SimpleValue.three, + child: Text(simpleValueToString( + context, + SimpleValue.three, + )), + ), + ], + child: ListTile( + title: Text( + GalleryLocalizations.of(context)!.demoMenuAnItemWithASimpleMenu), + subtitle: Text(simpleValueToString(context, _simpleValue)), + ), + ); + } +} + +// END + +// BEGIN menuDemoChecklist + +// Pressing the PopupMenuButton on the right of this item shows a menu +// whose items have checked icons that reflect this app's state. +class _ChecklistMenuDemo extends StatefulWidget { + const _ChecklistMenuDemo({required this.showInSnackBar}); + + final void Function(String value) showInSnackBar; + + @override + _ChecklistMenuDemoState createState() => _ChecklistMenuDemoState(); +} + +class _RestorableCheckedValues extends RestorableProperty> { + Set _checked = {}; + + void check(CheckedValue value) { + _checked.add(value); + notifyListeners(); + } + + void uncheck(CheckedValue value) { + _checked.remove(value); + notifyListeners(); + } + + bool isChecked(CheckedValue value) => _checked.contains(value); + + Iterable checkedValuesToString(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return _checked.map((CheckedValue value) { + return { + CheckedValue.one: localizations.demoMenuOne, + CheckedValue.two: localizations.demoMenuTwo, + CheckedValue.three: localizations.demoMenuThree, + CheckedValue.four: localizations.demoMenuFour, + }[value]!; + }); + } + + @override + Set createDefaultValue() => _checked; + + @override + Set initWithValue(Set a) { + _checked = a; + return _checked; + } + + @override + Object toPrimitives() => _checked.map((CheckedValue value) => value.index).toList(); + + @override + Set fromPrimitives(Object? data) { + final List checkedValues = data! as List; + return Set.from(checkedValues.map((dynamic id) { + return CheckedValue.values[id as int]; + })); + } +} + +class _ChecklistMenuDemoState extends State<_ChecklistMenuDemo> + with RestorationMixin { + final _RestorableCheckedValues _checkedValues = _RestorableCheckedValues() + ..check(CheckedValue.three); + + @override + String get restorationId => 'checklist_menu_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_checkedValues, 'checked_values'); + } + + void showCheckedMenuSelections(BuildContext context, CheckedValue value) { + if (_checkedValues.isChecked(value)) { + setState(() { + _checkedValues.uncheck(value); + }); + } else { + setState(() { + _checkedValues.check(value); + }); + } + + widget.showInSnackBar( + GalleryLocalizations.of(context)!.demoMenuChecked( + _checkedValues.checkedValuesToString(context), + ), + ); + } + + String checkedValueToString(BuildContext context, CheckedValue value) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return { + CheckedValue.one: localizations.demoMenuOne, + CheckedValue.two: localizations.demoMenuTwo, + CheckedValue.three: localizations.demoMenuThree, + CheckedValue.four: localizations.demoMenuFour, + }[value]!; + } + + @override + void dispose() { + _checkedValues.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text( + GalleryLocalizations.of(context)!.demoMenuAnItemWithAChecklistMenu, + ), + trailing: PopupMenuButton( + padding: EdgeInsets.zero, + onSelected: (CheckedValue value) => showCheckedMenuSelections(context, value), + itemBuilder: (BuildContext context) => >[ + CheckedPopupMenuItem( + value: CheckedValue.one, + checked: _checkedValues.isChecked(CheckedValue.one), + child: Text( + checkedValueToString(context, CheckedValue.one), + ), + ), + CheckedPopupMenuItem( + value: CheckedValue.two, + enabled: false, + checked: _checkedValues.isChecked(CheckedValue.two), + child: Text( + checkedValueToString(context, CheckedValue.two), + ), + ), + CheckedPopupMenuItem( + value: CheckedValue.three, + checked: _checkedValues.isChecked(CheckedValue.three), + child: Text( + checkedValueToString(context, CheckedValue.three), + ), + ), + CheckedPopupMenuItem( + value: CheckedValue.four, + checked: _checkedValues.isChecked(CheckedValue.four), + child: Text( + checkedValueToString(context, CheckedValue.four), + ), + ), + ], + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/material/navigation_drawer.dart b/dev/integration_tests/new_gallery/lib/demos/material/navigation_drawer.dart new file mode 100644 index 0000000000..7bc7332e91 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/navigation_drawer.dart @@ -0,0 +1,76 @@ +// 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:flutter/material.dart'; +import '../../gallery_localizations.dart'; + +// BEGIN navDrawerDemo + +// Press the Navigation Drawer button to the left of AppBar to show +// a simple Drawer with two items. +class NavDrawerDemo extends StatelessWidget { + const NavDrawerDemo({super.key}); + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localization = GalleryLocalizations.of(context)!; + final UserAccountsDrawerHeader drawerHeader = UserAccountsDrawerHeader( + accountName: Text( + localization.demoNavigationDrawerUserName, + ), + accountEmail: Text( + localization.demoNavigationDrawerUserEmail, + ), + currentAccountPicture: const CircleAvatar( + child: FlutterLogo(size: 42.0), + ), + ); + final ListView drawerItems = ListView( + children: [ + drawerHeader, + ListTile( + title: Text( + localization.demoNavigationDrawerToPageOne, + ), + leading: const Icon(Icons.favorite), + onTap: () { + Navigator.pop(context); + }, + ), + ListTile( + title: Text( + localization.demoNavigationDrawerToPageTwo, + ), + leading: const Icon(Icons.comment), + onTap: () { + Navigator.pop(context); + }, + ), + ], + ); + return Scaffold( + appBar: AppBar( + title: Text( + localization.demoNavigationDrawerTitle, + ), + ), + body: Semantics( + container: true, + child: Center( + child: Padding( + padding: const EdgeInsets.all(50.0), + child: Text( + localization.demoNavigationDrawerText, + ), + ), + ), + ), + drawer: Drawer( + child: drawerItems, + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/material/navigation_rail_demo.dart b/dev/integration_tests/new_gallery/lib/demos/material/navigation_rail_demo.dart new file mode 100644 index 0000000000..f00cfe3e47 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/navigation_rail_demo.dart @@ -0,0 +1,116 @@ +// 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:flutter/material.dart'; +import '../../gallery_localizations.dart'; + +// BEGIN navRailDemo + +class NavRailDemo extends StatefulWidget { + const NavRailDemo({super.key}); + + @override + State createState() => _NavRailDemoState(); +} + +class _NavRailDemoState extends State with RestorationMixin { + final RestorableInt _selectedIndex = RestorableInt(0); + + @override + String get restorationId => 'nav_rail_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_selectedIndex, 'selected_index'); + } + + @override + void dispose() { + _selectedIndex.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localization = GalleryLocalizations.of(context)!; + final String destinationFirst = localization.demoNavigationRailFirst; + final String destinationSecond = localization.demoNavigationRailSecond; + final String destinationThird = localization.demoNavigationRailThird; + final List selectedItem = [ + destinationFirst, + destinationSecond, + destinationThird + ]; + return Scaffold( + appBar: AppBar( + title: Text( + localization.demoNavigationRailTitle, + ), + ), + body: Row( + children: [ + NavigationRail( + leading: FloatingActionButton( + onPressed: () {}, + tooltip: localization.buttonTextCreate, + child: const Icon(Icons.add), + ), + selectedIndex: _selectedIndex.value, + onDestinationSelected: (int index) { + setState(() { + _selectedIndex.value = index; + }); + }, + labelType: NavigationRailLabelType.selected, + destinations: [ + NavigationRailDestination( + icon: const Icon( + Icons.favorite_border, + ), + selectedIcon: const Icon( + Icons.favorite, + ), + label: Text( + destinationFirst, + ), + ), + NavigationRailDestination( + icon: const Icon( + Icons.bookmark_border, + ), + selectedIcon: const Icon( + Icons.book, + ), + label: Text( + destinationSecond, + ), + ), + NavigationRailDestination( + icon: const Icon( + Icons.star_border, + ), + selectedIcon: const Icon( + Icons.star, + ), + label: Text( + destinationThird, + ), + ), + ], + ), + const VerticalDivider(thickness: 1, width: 1), + Expanded( + child: Center( + child: Text( + selectedItem[_selectedIndex.value], + ), + ), + ), + ], + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/material/picker_demo.dart b/dev/integration_tests/new_gallery/lib/demos/material/picker_demo.dart new file mode 100644 index 0000000000..98b073b639 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/picker_demo.dart @@ -0,0 +1,229 @@ +// 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:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import '../../gallery_localizations.dart'; +import 'material_demo_types.dart'; + +// BEGIN pickerDemo + +class PickerDemo extends StatefulWidget { + const PickerDemo({super.key, required this.type}); + + final PickerDemoType type; + + @override + State createState() => _PickerDemoState(); +} + +class _PickerDemoState extends State with RestorationMixin { + final RestorableDateTime _fromDate = RestorableDateTime(DateTime.now()); + final RestorableTimeOfDay _fromTime = RestorableTimeOfDay( + TimeOfDay.fromDateTime(DateTime.now()), + ); + final RestorableDateTime _startDate = RestorableDateTime(DateTime.now()); + final RestorableDateTime _endDate = RestorableDateTime(DateTime.now()); + + late RestorableRouteFuture _restorableDatePickerRouteFuture; + late RestorableRouteFuture + _restorableDateRangePickerRouteFuture; + late RestorableRouteFuture _restorableTimePickerRouteFuture; + + void _selectDate(DateTime? selectedDate) { + if (selectedDate != null && selectedDate != _fromDate.value) { + setState(() { + _fromDate.value = selectedDate; + }); + } + } + + void _selectDateRange(DateTimeRange? newSelectedDate) { + if (newSelectedDate != null) { + setState(() { + _startDate.value = newSelectedDate.start; + _endDate.value = newSelectedDate.end; + }); + } + } + + void _selectTime(TimeOfDay? selectedTime) { + if (selectedTime != null && selectedTime != _fromTime.value) { + setState(() { + _fromTime.value = selectedTime; + }); + } + } + + static Route _datePickerRoute( + BuildContext context, + Object? arguments, + ) { + return DialogRoute( + context: context, + builder: (BuildContext context) { + return DatePickerDialog( + restorationId: 'date_picker_dialog', + initialDate: DateTime.fromMillisecondsSinceEpoch(arguments! as int), + firstDate: DateTime(2015), + lastDate: DateTime(2100), + ); + }, + ); + } + + static Route _timePickerRoute( + BuildContext context, + Object? arguments, + ) { + final List args = arguments! as List; + final TimeOfDay initialTime = TimeOfDay( + hour: args[0] as int, + minute: args[1] as int, + ); + + return DialogRoute( + context: context, + builder: (BuildContext context) { + return TimePickerDialog( + restorationId: 'time_picker_dialog', + initialTime: initialTime, + ); + }, + ); + } + + static Route _dateRangePickerRoute( + BuildContext context, + Object? arguments, + ) { + return DialogRoute( + context: context, + builder: (BuildContext context) { + return DateRangePickerDialog( + restorationId: 'date_rage_picker_dialog', + firstDate: DateTime(DateTime.now().year - 5), + lastDate: DateTime(DateTime.now().year + 5), + ); + }, + ); + } + + @override + void initState() { + super.initState(); + _restorableDatePickerRouteFuture = RestorableRouteFuture( + onComplete: _selectDate, + onPresent: (NavigatorState navigator, Object? arguments) { + return navigator.restorablePush( + _datePickerRoute, + arguments: _fromDate.value.millisecondsSinceEpoch, + ); + }, + ); + _restorableDateRangePickerRouteFuture = + RestorableRouteFuture( + onComplete: _selectDateRange, + onPresent: (NavigatorState navigator, Object? arguments) => + navigator.restorablePush(_dateRangePickerRoute), + ); + + _restorableTimePickerRouteFuture = RestorableRouteFuture( + onComplete: _selectTime, + onPresent: (NavigatorState navigator, Object? arguments) => navigator.restorablePush( + _timePickerRoute, + arguments: [_fromTime.value.hour, _fromTime.value.minute], + ), + ); + } + + @override + String get restorationId => 'picker_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_fromDate, 'from_date'); + registerForRestoration(_fromTime, 'from_time'); + registerForRestoration(_startDate, 'start_date'); + registerForRestoration(_endDate, 'end_date'); + registerForRestoration( + _restorableDatePickerRouteFuture, + 'date_picker_route', + ); + registerForRestoration( + _restorableDateRangePickerRouteFuture, + 'date_range_picker_route', + ); + registerForRestoration( + _restorableTimePickerRouteFuture, + 'time_picker_route', + ); + } + + String get _title { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + switch (widget.type) { + case PickerDemoType.date: + return localizations.demoDatePickerTitle; + case PickerDemoType.time: + return localizations.demoTimePickerTitle; + case PickerDemoType.range: + return localizations.demoDateRangePickerTitle; + } + } + + String get _labelText { + switch (widget.type) { + case PickerDemoType.date: + return DateFormat.yMMMd().format(_fromDate.value); + case PickerDemoType.time: + return _fromTime.value.format(context); + case PickerDemoType.range: + return '${DateFormat.yMMMd().format(_startDate.value)} - ${DateFormat.yMMMd().format(_endDate.value)}'; + } + } + + @override + Widget build(BuildContext context) { + return Navigator( + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + builder: (BuildContext context) => Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(_title), + ), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(_labelText), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + switch (widget.type) { + case PickerDemoType.date: + _restorableDatePickerRouteFuture.present(); + case PickerDemoType.time: + _restorableTimePickerRouteFuture.present(); + case PickerDemoType.range: + _restorableDateRangePickerRouteFuture.present(); + } + }, + child: Text( + GalleryLocalizations.of(context)!.demoPickersShowPicker, + ), + ) + ], + ), + ), + ), + ); + }, + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/material/progress_indicator_demo.dart b/dev/integration_tests/new_gallery/lib/demos/material/progress_indicator_demo.dart new file mode 100644 index 0000000000..a38bc31633 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/progress_indicator_demo.dart @@ -0,0 +1,109 @@ +// 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:flutter/material.dart'; +import '../../gallery_localizations.dart'; +import 'material_demo_types.dart'; + +// BEGIN progressIndicatorsDemo + +class ProgressIndicatorDemo extends StatefulWidget { + const ProgressIndicatorDemo({super.key, required this.type}); + + final ProgressIndicatorDemoType type; + + @override + State createState() => _ProgressIndicatorDemoState(); +} + +class _ProgressIndicatorDemoState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + animationBehavior: AnimationBehavior.preserve, + )..forward(); + + _animation = CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.9, curve: Curves.fastOutSlowIn), + reverseCurve: Curves.fastOutSlowIn, + )..addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.dismissed) { + _controller.forward(); + } else if (status == AnimationStatus.completed) { + _controller.reverse(); + } + }); + } + + @override + void dispose() { + _controller.stop(); + super.dispose(); + } + + String get _title { + switch (widget.type) { + case ProgressIndicatorDemoType.circular: + return GalleryLocalizations.of(context)! + .demoCircularProgressIndicatorTitle; + case ProgressIndicatorDemoType.linear: + return GalleryLocalizations.of(context)! + .demoLinearProgressIndicatorTitle; + } + } + + Widget _buildIndicators(BuildContext context, Widget? child) { + switch (widget.type) { + case ProgressIndicatorDemoType.circular: + return Column( + children: [ + CircularProgressIndicator( + semanticsLabel: GalleryLocalizations.of(context)!.loading, + ), + const SizedBox(height: 32), + CircularProgressIndicator(value: _animation.value), + ], + ); + case ProgressIndicatorDemoType.linear: + return Column( + children: [ + const LinearProgressIndicator(), + const SizedBox(height: 32), + LinearProgressIndicator(value: _animation.value), + ], + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(_title), + ), + body: Center( + child: SingleChildScrollView( + child: Container( + padding: const EdgeInsets.all(8), + child: AnimatedBuilder( + animation: _animation, + builder: _buildIndicators, + ), + ), + ), + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/material/selection_controls_demo.dart b/dev/integration_tests/new_gallery/lib/demos/material/selection_controls_demo.dart new file mode 100644 index 0000000000..7d1ca8a6d3 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/selection_controls_demo.dart @@ -0,0 +1,278 @@ +// 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:flutter/material.dart'; +import '../../gallery_localizations.dart'; +import 'material_demo_types.dart'; + +class SelectionControlsDemo extends StatelessWidget { + const SelectionControlsDemo({super.key, required this.type}); + + final SelectionControlsDemoType type; + + String _title(BuildContext context) { + switch (type) { + case SelectionControlsDemoType.checkbox: + return GalleryLocalizations.of(context)! + .demoSelectionControlsCheckboxTitle; + case SelectionControlsDemoType.radio: + return GalleryLocalizations.of(context)! + .demoSelectionControlsRadioTitle; + case SelectionControlsDemoType.switches: + return GalleryLocalizations.of(context)! + .demoSelectionControlsSwitchTitle; + } + } + + @override + Widget build(BuildContext context) { + Widget? controls; + switch (type) { + case SelectionControlsDemoType.checkbox: + controls = _CheckboxDemo(); + case SelectionControlsDemoType.radio: + controls = _RadioDemo(); + case SelectionControlsDemoType.switches: + controls = _SwitchDemo(); + } + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(_title(context)), + ), + body: controls, + ); + } +} + +// BEGIN selectionControlsDemoCheckbox + +class _CheckboxDemo extends StatefulWidget { + @override + _CheckboxDemoState createState() => _CheckboxDemoState(); +} + +class _CheckboxDemoState extends State<_CheckboxDemo> with RestorationMixin { + RestorableBoolN checkboxValueA = RestorableBoolN(true); + RestorableBoolN checkboxValueB = RestorableBoolN(false); + RestorableBoolN checkboxValueC = RestorableBoolN(null); + + @override + String get restorationId => 'checkbox_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(checkboxValueA, 'checkbox_a'); + registerForRestoration(checkboxValueB, 'checkbox_b'); + registerForRestoration(checkboxValueC, 'checkbox_c'); + } + + @override + void dispose() { + checkboxValueA.dispose(); + checkboxValueB.dispose(); + checkboxValueC.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Checkbox( + value: checkboxValueA.value, + onChanged: (bool? value) { + setState(() { + checkboxValueA.value = value; + }); + }, + ), + Checkbox( + value: checkboxValueB.value, + onChanged: (bool? value) { + setState(() { + checkboxValueB.value = value; + }); + }, + ), + Checkbox( + value: checkboxValueC.value, + tristate: true, + onChanged: (bool? value) { + setState(() { + checkboxValueC.value = value; + }); + }, + ), + ], + ), + // Disabled checkboxes + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Checkbox( + value: checkboxValueA.value, + onChanged: null, + ), + Checkbox( + value: checkboxValueB.value, + onChanged: null, + ), + Checkbox( + value: checkboxValueC.value, + tristate: true, + onChanged: null, + ), + ], + ), + ], + ); + } +} + +// END + +// BEGIN selectionControlsDemoRadio + +class _RadioDemo extends StatefulWidget { + @override + _RadioDemoState createState() => _RadioDemoState(); +} + +class _RadioDemoState extends State<_RadioDemo> with RestorationMixin { + final RestorableInt radioValue = RestorableInt(0); + + @override + String get restorationId => 'radio_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(radioValue, 'radio_value'); + } + + void handleRadioValueChanged(int? value) { + setState(() { + radioValue.value = value!; + }); + } + + @override + void dispose() { + radioValue.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + for (int index = 0; index < 2; ++index) + Radio( + value: index, + groupValue: radioValue.value, + onChanged: handleRadioValueChanged, + ), + ], + ), + // Disabled radio buttons + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + for (int index = 0; index < 2; ++index) + Radio( + value: index, + groupValue: radioValue.value, + onChanged: null, + ), + ], + ), + ], + ); + } +} + +// END + +// BEGIN selectionControlsDemoSwitches + +class _SwitchDemo extends StatefulWidget { + @override + _SwitchDemoState createState() => _SwitchDemoState(); +} + +class _SwitchDemoState extends State<_SwitchDemo> with RestorationMixin { + RestorableBool switchValueA = RestorableBool(true); + RestorableBool switchValueB = RestorableBool(false); + + @override + String get restorationId => 'switch_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(switchValueA, 'switch_value1'); + registerForRestoration(switchValueB, 'switch_value2'); + } + + @override + void dispose() { + switchValueA.dispose(); + switchValueB.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Switch( + value: switchValueA.value, + onChanged: (bool value) { + setState(() { + switchValueA.value = value; + }); + }, + ), + Switch( + value: switchValueB.value, + onChanged: (bool value) { + setState(() { + switchValueB.value = value; + }); + }, + ), + ], + ), + // Disabled switches + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Switch( + value: switchValueA.value, + onChanged: null, + ), + Switch( + value: switchValueB.value, + onChanged: null, + ), + ], + ), + ], + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/material/sliders_demo.dart b/dev/integration_tests/new_gallery/lib/demos/material/sliders_demo.dart new file mode 100644 index 0000000000..a7ed6d8ebd --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/sliders_demo.dart @@ -0,0 +1,605 @@ +// 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 'dart:math' as math; + +import 'package:flutter/material.dart'; +import '../../gallery_localizations.dart'; +import 'material_demo_types.dart'; + +class SlidersDemo extends StatelessWidget { + const SlidersDemo({super.key, required this.type}); + + final SlidersDemoType type; + + String _title(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + switch (type) { + case SlidersDemoType.sliders: + return localizations.demoSlidersTitle; + case SlidersDemoType.rangeSliders: + return localizations.demoRangeSlidersTitle; + case SlidersDemoType.customSliders: + return localizations.demoCustomSlidersTitle; + } + } + + @override + Widget build(BuildContext context) { + Widget sliders; + switch (type) { + case SlidersDemoType.sliders: + sliders = _Sliders(); + case SlidersDemoType.rangeSliders: + sliders = _RangeSliders(); + case SlidersDemoType.customSliders: + sliders = _CustomSliders(); + } + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(_title(context)), + ), + body: sliders, + ); + } +} +// BEGIN slidersDemo + +class _Sliders extends StatefulWidget { + @override + _SlidersState createState() => _SlidersState(); +} + +class _SlidersState extends State<_Sliders> with RestorationMixin { + final RestorableDouble _continuousValue = RestorableDouble(25); + final RestorableDouble _discreteValue = RestorableDouble(20); + + @override + String get restorationId => 'slider_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_continuousValue, 'continuous_value'); + registerForRestoration(_discreteValue, 'discrete_value'); + } + + @override + void dispose() { + _continuousValue.dispose(); + _discreteValue.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Semantics( + label: localizations.demoSlidersEditableNumericalValue, + child: SizedBox( + width: 64, + height: 48, + child: TextField( + textAlign: TextAlign.center, + onSubmitted: (String value) { + final double? newValue = double.tryParse(value); + if (newValue != null && + newValue != _continuousValue.value) { + setState(() { + _continuousValue.value = + newValue.clamp(0, 100) as double; + }); + } + }, + keyboardType: TextInputType.number, + controller: TextEditingController( + text: _continuousValue.value.toStringAsFixed(0), + ), + ), + ), + ), + Slider( + value: _continuousValue.value, + max: 100, + onChanged: (double value) { + setState(() { + _continuousValue.value = value; + }); + }, + ), + // Disabled slider + Slider( + value: _continuousValue.value, + max: 100, + onChanged: null, + ), + Text(localizations + .demoSlidersContinuousWithEditableNumericalValue), + ], + ), + const SizedBox(height: 80), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Slider( + value: _discreteValue.value, + max: 200, + divisions: 5, + label: _discreteValue.value.round().toString(), + onChanged: (double value) { + setState(() { + _discreteValue.value = value; + }); + }, + ), + // Disabled slider + Slider( + value: _discreteValue.value, + max: 200, + divisions: 5, + label: _discreteValue.value.round().toString(), + onChanged: null, + ), + Text(localizations.demoSlidersDiscrete), + ], + ), + ], + ), + ); + } +} + +// END + +// BEGIN rangeSlidersDemo + +class _RangeSliders extends StatefulWidget { + @override + _RangeSlidersState createState() => _RangeSlidersState(); +} + +class _RangeSlidersState extends State<_RangeSliders> with RestorationMixin { + final RestorableDouble _continuousStartValue = RestorableDouble(25); + final RestorableDouble _continuousEndValue = RestorableDouble(75); + final RestorableDouble _discreteStartValue = RestorableDouble(40); + final RestorableDouble _discreteEndValue = RestorableDouble(120); + + @override + String get restorationId => 'range_sliders_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_continuousStartValue, 'continuous_start_value'); + registerForRestoration(_continuousEndValue, 'continuous_end_value'); + registerForRestoration(_discreteStartValue, 'discrete_start_value'); + registerForRestoration(_discreteEndValue, 'discrete_end_value'); + } + + @override + void dispose() { + _continuousStartValue.dispose(); + _continuousEndValue.dispose(); + _discreteStartValue.dispose(); + _discreteEndValue.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final RangeValues continuousValues = RangeValues( + _continuousStartValue.value, + _continuousEndValue.value, + ); + final RangeValues discreteValues = RangeValues( + _discreteStartValue.value, + _discreteEndValue.value, + ); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + RangeSlider( + values: continuousValues, + max: 100, + onChanged: (RangeValues values) { + setState(() { + _continuousStartValue.value = values.start; + _continuousEndValue.value = values.end; + }); + }, + ), + // Disabled range slider + RangeSlider( + values: continuousValues, + max: 100, + onChanged: null, + ), + Text(GalleryLocalizations.of(context)!.demoSlidersContinuous), + ], + ), + const SizedBox(height: 80), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + RangeSlider( + values: discreteValues, + max: 200, + divisions: 5, + labels: RangeLabels( + discreteValues.start.round().toString(), + discreteValues.end.round().toString(), + ), + onChanged: (RangeValues values) { + setState(() { + _discreteStartValue.value = values.start; + _discreteEndValue.value = values.end; + }); + }, + ), + // Disabled range slider + RangeSlider( + values: discreteValues, + max: 200, + divisions: 5, + labels: RangeLabels( + discreteValues.start.round().toString(), + discreteValues.end.round().toString(), + ), + onChanged: null, + ), + Text(GalleryLocalizations.of(context)!.demoSlidersDiscrete), + ], + ), + ], + ), + ); + } +} + +// END + +// BEGIN customSlidersDemo + +Path _downTriangle(double size, Offset thumbCenter, {bool invert = false}) { + final Path thumbPath = Path(); + final double height = math.sqrt(3) / 2; + final double centerHeight = size * height / 3; + final double halfSize = size / 2; + final int sign = invert ? -1 : 1; + thumbPath.moveTo( + thumbCenter.dx - halfSize, thumbCenter.dy + sign * centerHeight); + thumbPath.lineTo(thumbCenter.dx, thumbCenter.dy - 2 * sign * centerHeight); + thumbPath.lineTo( + thumbCenter.dx + halfSize, thumbCenter.dy + sign * centerHeight); + thumbPath.close(); + return thumbPath; +} + +Path _rightTriangle(double size, Offset thumbCenter, {bool invert = false}) { + final Path thumbPath = Path(); + final double halfSize = size / 2; + final int sign = invert ? -1 : 1; + thumbPath.moveTo(thumbCenter.dx + halfSize * sign, thumbCenter.dy); + thumbPath.lineTo(thumbCenter.dx - halfSize * sign, thumbCenter.dy - size); + thumbPath.lineTo(thumbCenter.dx - halfSize * sign, thumbCenter.dy + size); + thumbPath.close(); + return thumbPath; +} + +Path _upTriangle(double size, Offset thumbCenter) => + _downTriangle(size, thumbCenter, invert: true); + +Path _leftTriangle(double size, Offset thumbCenter) => + _rightTriangle(size, thumbCenter, invert: true); + +class _CustomRangeThumbShape extends RangeSliderThumbShape { + const _CustomRangeThumbShape(); + + static const double _thumbSize = 4; + static const double _disabledThumbSize = 3; + + @override + Size getPreferredSize(bool isEnabled, bool isDiscrete) { + return isEnabled + ? const Size.fromRadius(_thumbSize) + : const Size.fromRadius(_disabledThumbSize); + } + + static final Animatable sizeTween = Tween( + begin: _disabledThumbSize, + end: _thumbSize, + ); + + @override + void paint( + PaintingContext context, + Offset center, { + required Animation activationAnimation, + required Animation enableAnimation, + bool isDiscrete = false, + bool isEnabled = false, + bool? isOnTop, + TextDirection? textDirection, + required SliderThemeData sliderTheme, + Thumb? thumb, + bool? isPressed, + }) { + final Canvas canvas = context.canvas; + final ColorTween colorTween = ColorTween( + begin: sliderTheme.disabledThumbColor, + end: sliderTheme.thumbColor, + ); + + final double size = _thumbSize * sizeTween.evaluate(enableAnimation); + Path thumbPath; + switch (textDirection!) { + case TextDirection.rtl: + switch (thumb!) { + case Thumb.start: + thumbPath = _rightTriangle(size, center); + case Thumb.end: + thumbPath = _leftTriangle(size, center); + } + case TextDirection.ltr: + switch (thumb!) { + case Thumb.start: + thumbPath = _leftTriangle(size, center); + case Thumb.end: + thumbPath = _rightTriangle(size, center); + } + } + canvas.drawPath( + thumbPath, + Paint()..color = colorTween.evaluate(enableAnimation)!, + ); + } +} + +class _CustomThumbShape extends SliderComponentShape { + const _CustomThumbShape(); + + static const double _thumbSize = 4; + static const double _disabledThumbSize = 3; + + @override + Size getPreferredSize(bool isEnabled, bool isDiscrete) { + return isEnabled + ? const Size.fromRadius(_thumbSize) + : const Size.fromRadius(_disabledThumbSize); + } + + static final Animatable sizeTween = Tween( + begin: _disabledThumbSize, + end: _thumbSize, + ); + + @override + void paint( + PaintingContext context, + Offset thumbCenter, { + Animation? activationAnimation, + required Animation enableAnimation, + bool? isDiscrete, + TextPainter? labelPainter, + RenderBox? parentBox, + required SliderThemeData sliderTheme, + TextDirection? textDirection, + double? value, + double? textScaleFactor, + Size? sizeWithOverflow, + }) { + final Canvas canvas = context.canvas; + final ColorTween colorTween = ColorTween( + begin: sliderTheme.disabledThumbColor, + end: sliderTheme.thumbColor, + ); + final double size = _thumbSize * sizeTween.evaluate(enableAnimation); + final Path thumbPath = _downTriangle(size, thumbCenter); + canvas.drawPath( + thumbPath, + Paint()..color = colorTween.evaluate(enableAnimation)!, + ); + } +} + +class _CustomValueIndicatorShape extends SliderComponentShape { + const _CustomValueIndicatorShape(); + + static const double _indicatorSize = 4; + static const double _disabledIndicatorSize = 3; + static const double _slideUpHeight = 40; + + @override + Size getPreferredSize(bool isEnabled, bool isDiscrete) { + return Size.fromRadius(isEnabled ? _indicatorSize : _disabledIndicatorSize); + } + + static final Animatable sizeTween = Tween( + begin: _disabledIndicatorSize, + end: _indicatorSize, + ); + + @override + void paint( + PaintingContext context, + Offset thumbCenter, { + required Animation activationAnimation, + required Animation enableAnimation, + bool? isDiscrete, + required TextPainter labelPainter, + RenderBox? parentBox, + required SliderThemeData sliderTheme, + TextDirection? textDirection, + double? value, + double? textScaleFactor, + Size? sizeWithOverflow, + }) { + final Canvas canvas = context.canvas; + final ColorTween enableColor = ColorTween( + begin: sliderTheme.disabledThumbColor, + end: sliderTheme.valueIndicatorColor, + ); + final Tween slideUpTween = Tween( + begin: 0, + end: _slideUpHeight, + ); + final double size = _indicatorSize * sizeTween.evaluate(enableAnimation); + final Offset slideUpOffset = + Offset(0, -slideUpTween.evaluate(activationAnimation)); + final Path thumbPath = _upTriangle(size, thumbCenter + slideUpOffset); + final Color paintColor = enableColor + .evaluate(enableAnimation)! + .withAlpha((255 * activationAnimation.value).round()); + canvas.drawPath( + thumbPath, + Paint()..color = paintColor, + ); + canvas.drawLine( + thumbCenter, + thumbCenter + slideUpOffset, + Paint() + ..color = paintColor + ..style = PaintingStyle.stroke + ..strokeWidth = 2); + labelPainter.paint( + canvas, + thumbCenter + + slideUpOffset + + Offset(-labelPainter.width / 2, -labelPainter.height - 4), + ); + } +} + +class _CustomSliders extends StatefulWidget { + @override + _CustomSlidersState createState() => _CustomSlidersState(); +} + +class _CustomSlidersState extends State<_CustomSliders> with RestorationMixin { + final RestorableDouble _continuousStartCustomValue = RestorableDouble(40); + final RestorableDouble _continuousEndCustomValue = RestorableDouble(160); + final RestorableDouble _discreteCustomValue = RestorableDouble(25); + + @override + String get restorationId => 'custom_sliders_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration( + _continuousStartCustomValue, 'continuous_start_custom_value'); + registerForRestoration( + _continuousEndCustomValue, 'continuous_end_custom_value'); + registerForRestoration(_discreteCustomValue, 'discrete_custom_value'); + } + + @override + void dispose() { + _continuousStartCustomValue.dispose(); + _continuousEndCustomValue.dispose(); + _discreteCustomValue.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final RangeValues customRangeValue = RangeValues( + _continuousStartCustomValue.value, + _continuousEndCustomValue.value, + ); + final ThemeData theme = Theme.of(context); + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + SliderTheme( + data: theme.sliderTheme.copyWith( + trackHeight: 2, + activeTrackColor: Colors.deepPurple, + inactiveTrackColor: + theme.colorScheme.onSurface.withOpacity(0.5), + activeTickMarkColor: + theme.colorScheme.onSurface.withOpacity(0.7), + inactiveTickMarkColor: + theme.colorScheme.surface.withOpacity(0.7), + overlayColor: theme.colorScheme.onSurface.withOpacity(0.12), + thumbColor: Colors.deepPurple, + valueIndicatorColor: Colors.deepPurpleAccent, + thumbShape: const _CustomThumbShape(), + valueIndicatorShape: const _CustomValueIndicatorShape(), + valueIndicatorTextStyle: theme.textTheme.bodyLarge! + .copyWith(color: theme.colorScheme.onSurface), + ), + child: Slider( + value: _discreteCustomValue.value, + max: 200, + divisions: 5, + semanticFormatterCallback: (double value) => + value.round().toString(), + label: '${_discreteCustomValue.value.round()}', + onChanged: (double value) { + setState(() { + _discreteCustomValue.value = value; + }); + }, + ), + ), + Text(localizations.demoSlidersDiscreteSliderWithCustomTheme), + ], + ), + const SizedBox(height: 80), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + SliderTheme( + data: const SliderThemeData( + trackHeight: 2, + activeTrackColor: Colors.deepPurple, + inactiveTrackColor: Colors.black26, + activeTickMarkColor: Colors.white70, + inactiveTickMarkColor: Colors.black, + overlayColor: Colors.black12, + thumbColor: Colors.deepPurple, + rangeThumbShape: _CustomRangeThumbShape(), + showValueIndicator: ShowValueIndicator.never, + ), + child: RangeSlider( + values: customRangeValue, + max: 200, + onChanged: (RangeValues values) { + setState(() { + _continuousStartCustomValue.value = values.start; + _continuousEndCustomValue.value = values.end; + }); + }, + ), + ), + Text(localizations + .demoSlidersContinuousRangeSliderWithCustomTheme), + ], + ), + ], + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/material/snackbar_demo.dart b/dev/integration_tests/new_gallery/lib/demos/material/snackbar_demo.dart new file mode 100644 index 0000000000..2343e955ec --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/snackbar_demo.dart @@ -0,0 +1,46 @@ +// 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:flutter/material.dart'; +import '../../gallery_localizations.dart'; + +// BEGIN snackbarsDemo + +class SnackbarsDemo extends StatelessWidget { + const SnackbarsDemo({super.key}); + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(localizations.demoSnackbarsTitle), + ), + body: Center( + child: ElevatedButton( + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(localizations.demoSnackbarsText), + action: SnackBarAction( + label: localizations.demoSnackbarsActionButtonLabel, + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + localizations.demoSnackbarsAction, + ))); + }, + ), + )); + }, + child: Text(localizations.demoSnackbarsButtonLabel), + ), + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/material/tabs_demo.dart b/dev/integration_tests/new_gallery/lib/demos/material/tabs_demo.dart new file mode 100644 index 0000000000..be5cf90e20 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/tabs_demo.dart @@ -0,0 +1,197 @@ +// 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:flutter/material.dart'; +import '../../gallery_localizations.dart'; +import 'material_demo_types.dart'; + +class TabsDemo extends StatelessWidget { + const TabsDemo({super.key, required this.type}); + + final TabsDemoType type; + + @override + Widget build(BuildContext context) { + Widget tabs; + switch (type) { + case TabsDemoType.scrollable: + tabs = _TabsScrollableDemo(); + case TabsDemoType.nonScrollable: + tabs = _TabsNonScrollableDemo(); + } + return tabs; + } +} + +// BEGIN tabsScrollableDemo + +class _TabsScrollableDemo extends StatefulWidget { + @override + __TabsScrollableDemoState createState() => __TabsScrollableDemoState(); +} + +class __TabsScrollableDemoState extends State<_TabsScrollableDemo> + with SingleTickerProviderStateMixin, RestorationMixin { + TabController? _tabController; + + final RestorableInt tabIndex = RestorableInt(0); + + @override + String get restorationId => 'tab_scrollable_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(tabIndex, 'tab_index'); + _tabController!.index = tabIndex.value; + } + + @override + void initState() { + _tabController = TabController( + length: 12, + vsync: this, + ); + _tabController!.addListener(() { + // When the tab controller's value is updated, make sure to update the + // tab index value, which is state restorable. + setState(() { + tabIndex.value = _tabController!.index; + }); + }); + super.initState(); + } + + @override + void dispose() { + _tabController!.dispose(); + tabIndex.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + final List tabs = [ + localizations.colorsRed, + localizations.colorsOrange, + localizations.colorsGreen, + localizations.colorsBlue, + localizations.colorsIndigo, + localizations.colorsPurple, + localizations.colorsRed, + localizations.colorsOrange, + localizations.colorsGreen, + localizations.colorsBlue, + localizations.colorsIndigo, + localizations.colorsPurple, + ]; + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(localizations.demoTabsScrollingTitle), + bottom: TabBar( + controller: _tabController, + isScrollable: true, + tabs: [ + for (final String tab in tabs) Tab(text: tab), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + for (final String tab in tabs) + Center( + child: Text(tab), + ), + ], + ), + ); + } +} + +// END + +// BEGIN tabsNonScrollableDemo + +class _TabsNonScrollableDemo extends StatefulWidget { + @override + __TabsNonScrollableDemoState createState() => __TabsNonScrollableDemoState(); +} + +class __TabsNonScrollableDemoState extends State<_TabsNonScrollableDemo> + with SingleTickerProviderStateMixin, RestorationMixin { + late TabController _tabController; + + final RestorableInt tabIndex = RestorableInt(0); + + @override + String get restorationId => 'tab_non_scrollable_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(tabIndex, 'tab_index'); + _tabController.index = tabIndex.value; + } + + @override + void initState() { + super.initState(); + _tabController = TabController( + length: 3, + vsync: this, + ); + _tabController.addListener(() { + // When the tab controller's value is updated, make sure to update the + // tab index value, which is state restorable. + setState(() { + tabIndex.value = _tabController.index; + }); + }); + } + + @override + void dispose() { + _tabController.dispose(); + tabIndex.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + final List tabs = [ + localizations.colorsRed, + localizations.colorsOrange, + localizations.colorsGreen, + ]; + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text( + localizations.demoTabsNonScrollingTitle, + ), + bottom: TabBar( + controller: _tabController, + tabs: [ + for (final String tab in tabs) Tab(text: tab), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + for (final String tab in tabs) + Center( + child: Text(tab), + ), + ], + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/material/text_field_demo.dart b/dev/integration_tests/new_gallery/lib/demos/material/text_field_demo.dart new file mode 100644 index 0000000000..94b8634c78 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/text_field_demo.dart @@ -0,0 +1,422 @@ +// 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:flutter/material.dart'; +import 'package:flutter/services.dart'; +import '../../gallery_localizations.dart'; + +// BEGIN textFieldDemo + +class TextFieldDemo extends StatelessWidget { + const TextFieldDemo({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(GalleryLocalizations.of(context)!.demoTextFieldTitle), + ), + body: const TextFormFieldDemo(), + ); + } +} + +class TextFormFieldDemo extends StatefulWidget { + const TextFormFieldDemo({super.key}); + + @override + TextFormFieldDemoState createState() => TextFormFieldDemoState(); +} + +class PersonData { + String? name = ''; + String? phoneNumber = ''; + String? email = ''; + String password = ''; +} + +class PasswordField extends StatefulWidget { + const PasswordField({ + super.key, + this.restorationId, + this.fieldKey, + this.hintText, + this.labelText, + this.helperText, + this.onSaved, + this.validator, + this.onFieldSubmitted, + this.focusNode, + this.textInputAction, + }); + + final String? restorationId; + final Key? fieldKey; + final String? hintText; + final String? labelText; + final String? helperText; + final FormFieldSetter? onSaved; + final FormFieldValidator? validator; + final ValueChanged? onFieldSubmitted; + final FocusNode? focusNode; + final TextInputAction? textInputAction; + + @override + State createState() => _PasswordFieldState(); +} + +class _PasswordFieldState extends State with RestorationMixin { + final RestorableBool _obscureText = RestorableBool(true); + + @override + String? get restorationId => widget.restorationId; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_obscureText, 'obscure_text'); + } + + @override + Widget build(BuildContext context) { + return TextFormField( + key: widget.fieldKey, + restorationId: 'password_text_field', + obscureText: _obscureText.value, + maxLength: 8, + onSaved: widget.onSaved, + validator: widget.validator, + onFieldSubmitted: widget.onFieldSubmitted, + decoration: InputDecoration( + filled: true, + hintText: widget.hintText, + labelText: widget.labelText, + helperText: widget.helperText, + suffixIcon: IconButton( + onPressed: () { + setState(() { + _obscureText.value = !_obscureText.value; + }); + }, + hoverColor: Colors.transparent, + icon: Icon( + _obscureText.value ? Icons.visibility : Icons.visibility_off, + semanticLabel: _obscureText.value + ? GalleryLocalizations.of(context)! + .demoTextFieldShowPasswordLabel + : GalleryLocalizations.of(context)! + .demoTextFieldHidePasswordLabel, + ), + ), + ), + ); + } +} + +class TextFormFieldDemoState extends State + with RestorationMixin { + PersonData person = PersonData(); + + late FocusNode _phoneNumber, _email, _lifeStory, _password, _retypePassword; + + @override + void initState() { + super.initState(); + _phoneNumber = FocusNode(); + _email = FocusNode(); + _lifeStory = FocusNode(); + _password = FocusNode(); + _retypePassword = FocusNode(); + } + + @override + void dispose() { + _phoneNumber.dispose(); + _email.dispose(); + _lifeStory.dispose(); + _password.dispose(); + _retypePassword.dispose(); + super.dispose(); + } + + void showInSnackBar(String value) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(value), + )); + } + + @override + String get restorationId => 'text_field_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_autoValidateModeIndex, 'autovalidate_mode'); + } + + final RestorableInt _autoValidateModeIndex = + RestorableInt(AutovalidateMode.disabled.index); + + final GlobalKey _formKey = GlobalKey(); + final GlobalKey> _passwordFieldKey = + GlobalKey>(); + final _UsNumberTextInputFormatter _phoneNumberFormatter = + _UsNumberTextInputFormatter(); + + void _handleSubmitted() { + final FormState form = _formKey.currentState!; + if (!form.validate()) { + _autoValidateModeIndex.value = + AutovalidateMode.always.index; // Start validating on every change. + showInSnackBar( + GalleryLocalizations.of(context)!.demoTextFieldFormErrors, + ); + } else { + form.save(); + showInSnackBar(GalleryLocalizations.of(context)! + .demoTextFieldNameHasPhoneNumber(person.name!, person.phoneNumber!)); + } + } + + String? _validateName(String? value) { + if (value == null || value.isEmpty) { + return GalleryLocalizations.of(context)!.demoTextFieldNameRequired; + } + final RegExp nameExp = RegExp(r'^[A-Za-z ]+$'); + if (!nameExp.hasMatch(value)) { + return GalleryLocalizations.of(context)! + .demoTextFieldOnlyAlphabeticalChars; + } + return null; + } + + String? _validatePhoneNumber(String? value) { + final RegExp phoneExp = RegExp(r'^\(\d\d\d\) \d\d\d\-\d\d\d\d$'); + if (!phoneExp.hasMatch(value!)) { + return GalleryLocalizations.of(context)!.demoTextFieldEnterUSPhoneNumber; + } + return null; + } + + String? _validatePassword(String? value) { + final FormFieldState passwordField = _passwordFieldKey.currentState!; + if (passwordField.value == null || passwordField.value!.isEmpty) { + return GalleryLocalizations.of(context)!.demoTextFieldEnterPassword; + } + if (passwordField.value != value) { + return GalleryLocalizations.of(context)!.demoTextFieldPasswordsDoNotMatch; + } + return null; + } + + @override + Widget build(BuildContext context) { + const SizedBox sizedBoxSpace = SizedBox(height: 24); + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + return Form( + key: _formKey, + autovalidateMode: AutovalidateMode.values[_autoValidateModeIndex.value], + child: Scrollbar( + child: SingleChildScrollView( + restorationId: 'text_field_demo_scroll_view', + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + sizedBoxSpace, + TextFormField( + restorationId: 'name_field', + textInputAction: TextInputAction.next, + textCapitalization: TextCapitalization.words, + decoration: InputDecoration( + filled: true, + icon: const Icon(Icons.person), + hintText: localizations.demoTextFieldWhatDoPeopleCallYou, + labelText: localizations.demoTextFieldNameField, + ), + onSaved: (String? value) { + person.name = value; + _phoneNumber.requestFocus(); + }, + validator: _validateName, + ), + sizedBoxSpace, + TextFormField( + restorationId: 'phone_number_field', + textInputAction: TextInputAction.next, + focusNode: _phoneNumber, + decoration: InputDecoration( + filled: true, + icon: const Icon(Icons.phone), + hintText: localizations.demoTextFieldWhereCanWeReachYou, + labelText: localizations.demoTextFieldPhoneNumber, + prefixText: '+1 ', + ), + keyboardType: TextInputType.phone, + onSaved: (String? value) { + person.phoneNumber = value; + _email.requestFocus(); + }, + maxLength: 14, + maxLengthEnforcement: MaxLengthEnforcement.none, + validator: _validatePhoneNumber, + // TextInputFormatters are applied in sequence. + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + // Fit the validating format. + _phoneNumberFormatter, + ], + ), + sizedBoxSpace, + TextFormField( + restorationId: 'email_field', + textInputAction: TextInputAction.next, + focusNode: _email, + decoration: InputDecoration( + filled: true, + icon: const Icon(Icons.email), + hintText: localizations.demoTextFieldYourEmailAddress, + labelText: localizations.demoTextFieldEmail, + ), + keyboardType: TextInputType.emailAddress, + onSaved: (String? value) { + person.email = value; + _lifeStory.requestFocus(); + }, + ), + sizedBoxSpace, + // Disabled text field + TextFormField( + enabled: false, + restorationId: 'disabled_email_field', + textInputAction: TextInputAction.next, + decoration: InputDecoration( + filled: true, + icon: const Icon(Icons.email), + hintText: localizations.demoTextFieldYourEmailAddress, + labelText: localizations.demoTextFieldEmail, + ), + keyboardType: TextInputType.emailAddress, + ), + sizedBoxSpace, + TextFormField( + restorationId: 'life_story_field', + focusNode: _lifeStory, + decoration: InputDecoration( + border: const OutlineInputBorder(), + hintText: localizations.demoTextFieldTellUsAboutYourself, + helperText: localizations.demoTextFieldKeepItShort, + labelText: localizations.demoTextFieldLifeStory, + ), + maxLines: 3, + ), + sizedBoxSpace, + TextFormField( + restorationId: 'salary_field', + textInputAction: TextInputAction.next, + keyboardType: TextInputType.number, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: localizations.demoTextFieldSalary, + suffixText: localizations.demoTextFieldUSD, + ), + ), + sizedBoxSpace, + PasswordField( + restorationId: 'password_field', + textInputAction: TextInputAction.next, + focusNode: _password, + fieldKey: _passwordFieldKey, + helperText: localizations.demoTextFieldNoMoreThan, + labelText: localizations.demoTextFieldPassword, + onFieldSubmitted: (String value) { + setState(() { + person.password = value; + _retypePassword.requestFocus(); + }); + }, + ), + sizedBoxSpace, + TextFormField( + restorationId: 'retype_password_field', + focusNode: _retypePassword, + decoration: InputDecoration( + filled: true, + labelText: localizations.demoTextFieldRetypePassword, + ), + maxLength: 8, + obscureText: true, + validator: _validatePassword, + onFieldSubmitted: (String value) { + _handleSubmitted(); + }, + ), + sizedBoxSpace, + Center( + child: ElevatedButton( + onPressed: _handleSubmitted, + child: Text(localizations.demoTextFieldSubmit), + ), + ), + sizedBoxSpace, + Text( + localizations.demoTextFieldRequiredField, + style: Theme.of(context).textTheme.bodySmall, + ), + sizedBoxSpace, + ], + ), + ), + ), + ); + } +} + +/// Format incoming numeric text to fit the format of (###) ###-#### ## +class _UsNumberTextInputFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + final int newTextLength = newValue.text.length; + final StringBuffer newText = StringBuffer(); + int selectionIndex = newValue.selection.end; + int usedSubstringIndex = 0; + if (newTextLength >= 1) { + newText.write('('); + if (newValue.selection.end >= 1) { + selectionIndex++; + } + } + if (newTextLength >= 4) { + newText.write('${newValue.text.substring(0, usedSubstringIndex = 3)}) '); + if (newValue.selection.end >= 3) { + selectionIndex += 2; + } + } + if (newTextLength >= 7) { + newText.write('${newValue.text.substring(3, usedSubstringIndex = 6)}-'); + if (newValue.selection.end >= 6) { + selectionIndex++; + } + } + if (newTextLength >= 11) { + newText.write('${newValue.text.substring(6, usedSubstringIndex = 10)} '); + if (newValue.selection.end >= 10) { + selectionIndex++; + } + } + // Dump the rest. + if (newTextLength >= usedSubstringIndex) { + newText.write(newValue.text.substring(usedSubstringIndex)); + } + return TextEditingValue( + text: newText.toString(), + selection: TextSelection.collapsed(offset: selectionIndex), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/material/tooltip_demo.dart b/dev/integration_tests/new_gallery/lib/demos/material/tooltip_demo.dart new file mode 100644 index 0000000000..de13632518 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/material/tooltip_demo.dart @@ -0,0 +1,48 @@ +// 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:flutter/material.dart'; +import '../../gallery_localizations.dart'; + +// BEGIN tooltipDemo + +class TooltipDemo extends StatelessWidget { + const TooltipDemo({super.key}); + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(localizations.demoTooltipTitle), + ), + body: Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + localizations.demoTooltipInstructions, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Tooltip( + message: localizations.starterAppTooltipSearch, + child: IconButton( + color: Theme.of(context).colorScheme.primary, + onPressed: () {}, + icon: const Icon(Icons.search), + ), + ), + ], + ), + ), + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/reference/colors_demo.dart b/dev/integration_tests/new_gallery/lib/demos/reference/colors_demo.dart new file mode 100644 index 0000000000..af5daf4036 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/reference/colors_demo.dart @@ -0,0 +1,260 @@ +// 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:flutter/material.dart'; + +import '../../gallery_localizations.dart'; + +// BEGIN colorsDemo + +const double kColorItemHeight = 48; + +class _Palette { + _Palette({ + required this.name, + required this.primary, + this.accent, + this.threshold = 900, + }); + + final String name; + final MaterialColor primary; + final MaterialAccentColor? accent; + + // Titles for indices > threshold are white, otherwise black. + final int threshold; +} + +List<_Palette> _allPalettes(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return <_Palette>[ + _Palette( + name: localizations.colorsRed, + primary: Colors.red, + accent: Colors.redAccent, + threshold: 300, + ), + _Palette( + name: localizations.colorsPink, + primary: Colors.pink, + accent: Colors.pinkAccent, + threshold: 200, + ), + _Palette( + name: localizations.colorsPurple, + primary: Colors.purple, + accent: Colors.purpleAccent, + threshold: 200, + ), + _Palette( + name: localizations.colorsDeepPurple, + primary: Colors.deepPurple, + accent: Colors.deepPurpleAccent, + threshold: 200, + ), + _Palette( + name: localizations.colorsIndigo, + primary: Colors.indigo, + accent: Colors.indigoAccent, + threshold: 200, + ), + _Palette( + name: localizations.colorsBlue, + primary: Colors.blue, + accent: Colors.blueAccent, + threshold: 400, + ), + _Palette( + name: localizations.colorsLightBlue, + primary: Colors.lightBlue, + accent: Colors.lightBlueAccent, + threshold: 500, + ), + _Palette( + name: localizations.colorsCyan, + primary: Colors.cyan, + accent: Colors.cyanAccent, + threshold: 600, + ), + _Palette( + name: localizations.colorsTeal, + primary: Colors.teal, + accent: Colors.tealAccent, + threshold: 400, + ), + _Palette( + name: localizations.colorsGreen, + primary: Colors.green, + accent: Colors.greenAccent, + threshold: 500, + ), + _Palette( + name: localizations.colorsLightGreen, + primary: Colors.lightGreen, + accent: Colors.lightGreenAccent, + threshold: 600, + ), + _Palette( + name: localizations.colorsLime, + primary: Colors.lime, + accent: Colors.limeAccent, + threshold: 800, + ), + _Palette( + name: localizations.colorsYellow, + primary: Colors.yellow, + accent: Colors.yellowAccent, + ), + _Palette( + name: localizations.colorsAmber, + primary: Colors.amber, + accent: Colors.amberAccent, + ), + _Palette( + name: localizations.colorsOrange, + primary: Colors.orange, + accent: Colors.orangeAccent, + threshold: 700, + ), + _Palette( + name: localizations.colorsDeepOrange, + primary: Colors.deepOrange, + accent: Colors.deepOrangeAccent, + threshold: 400, + ), + _Palette( + name: localizations.colorsBrown, + primary: Colors.brown, + threshold: 200, + ), + _Palette( + name: localizations.colorsGrey, + primary: Colors.grey, + threshold: 500, + ), + _Palette( + name: localizations.colorsBlueGrey, + primary: Colors.blueGrey, + threshold: 500, + ), + ]; +} + +class _ColorItem extends StatelessWidget { + const _ColorItem({ + required this.index, + required this.color, + this.prefix = '', + }); + + final int index; + final Color color; + final String prefix; + + String get _colorString => + "#${color.value.toRadixString(16).padLeft(8, '0').toUpperCase()}"; + + @override + Widget build(BuildContext context) { + return Semantics( + container: true, + child: Container( + height: kColorItemHeight, + padding: const EdgeInsets.symmetric(horizontal: 16), + color: color, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('$prefix$index'), + Flexible(child: Text(_colorString)), + ], + ), + ), + ); + } +} + +class _PaletteTabView extends StatelessWidget { + const _PaletteTabView({required this.colors}); + + final _Palette colors; + static const List primaryKeys = [ + 50, + 100, + 200, + 300, + 400, + 500, + 600, + 700, + 800, + 900 + ]; + static const List accentKeys = [100, 200, 400, 700]; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + final TextStyle whiteTextStyle = textTheme.bodyMedium!.copyWith( + color: Colors.white, + ); + final TextStyle blackTextStyle = textTheme.bodyMedium!.copyWith( + color: Colors.black, + ); + return Scrollbar( + child: ListView( + itemExtent: kColorItemHeight, + children: [ + for (final int key in primaryKeys) + DefaultTextStyle( + style: key > colors.threshold ? whiteTextStyle : blackTextStyle, + child: _ColorItem(index: key, color: colors.primary[key]!), + ), + if (colors.accent != null) + for (final int key in accentKeys) + DefaultTextStyle( + style: key > colors.threshold ? whiteTextStyle : blackTextStyle, + child: _ColorItem( + index: key, + color: colors.accent![key]!, + prefix: 'A', + ), + ), + ], + ), + ); + } +} + +class ColorsDemo extends StatelessWidget { + const ColorsDemo({super.key}); + + @override + Widget build(BuildContext context) { + final List<_Palette> palettes = _allPalettes(context); + return DefaultTabController( + length: palettes.length, + child: Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(GalleryLocalizations.of(context)!.demoColorsTitle), + bottom: TabBar( + isScrollable: true, + tabs: [ + for (final _Palette palette in palettes) Tab(text: palette.name), + ], + labelColor: Theme.of(context).colorScheme.onPrimary, + ), + ), + body: TabBarView( + children: [ + for (final _Palette palette in palettes) _PaletteTabView(colors: palette), + ], + ), + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/reference/motion_demo_container_transition.dart b/dev/integration_tests/new_gallery/lib/demos/reference/motion_demo_container_transition.dart new file mode 100644 index 0000000000..161546a6ce --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/reference/motion_demo_container_transition.dart @@ -0,0 +1,590 @@ +// 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 '../../gallery_localizations.dart'; + +// BEGIN openContainerTransformDemo + +const String _loremIpsumParagraph = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod ' + 'tempor incididunt ut labore et dolore magna aliqua. Vulputate dignissim ' + 'suspendisse in est. Ut ornare lectus sit amet. Eget nunc lobortis mattis ' + 'aliquam faucibus purus in. Hendrerit gravida rutrum quisque non tellus ' + 'orci ac auctor. Mattis aliquam faucibus purus in massa. Tellus rutrum ' + 'tellus pellentesque eu tincidunt tortor. Nunc eget lorem dolor sed. Nulla ' + 'at volutpat diam ut venenatis tellus in metus. Tellus cras adipiscing enim ' + 'eu turpis. Pretium fusce id velit ut tortor. Adipiscing enim eu turpis ' + 'egestas pretium. Quis varius quam quisque id. Blandit aliquam etiam erat ' + 'velit scelerisque. In nisl nisi scelerisque eu. Semper risus in hendrerit ' + 'gravida rutrum quisque. Suspendisse in est ante in nibh mauris cursus ' + 'mattis molestie. Adipiscing elit duis tristique sollicitudin nibh sit ' + 'amet commodo nulla. Pretium viverra suspendisse potenti nullam ac tortor ' + 'vitae.\n' + '\n' + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod ' + 'tempor incididunt ut labore et dolore magna aliqua. Vulputate dignissim ' + 'suspendisse in est. Ut ornare lectus sit amet. Eget nunc lobortis mattis ' + 'aliquam faucibus purus in. Hendrerit gravida rutrum quisque non tellus ' + 'orci ac auctor. Mattis aliquam faucibus purus in massa. Tellus rutrum ' + 'tellus pellentesque eu tincidunt tortor. Nunc eget lorem dolor sed. Nulla ' + 'at volutpat diam ut venenatis tellus in metus. Tellus cras adipiscing enim ' + 'eu turpis. Pretium fusce id velit ut tortor. Adipiscing enim eu turpis ' + 'egestas pretium. Quis varius quam quisque id. Blandit aliquam etiam erat ' + 'velit scelerisque. In nisl nisi scelerisque eu. Semper risus in hendrerit ' + 'gravida rutrum quisque. Suspendisse in est ante in nibh mauris cursus ' + 'mattis molestie. Adipiscing elit duis tristique sollicitudin nibh sit ' + 'amet commodo nulla. Pretium viverra suspendisse potenti nullam ac tortor ' + 'vitae'; + +const double _fabDimension = 56; + +class OpenContainerTransformDemo extends StatefulWidget { + const OpenContainerTransformDemo({super.key}); + + @override + State createState() => + _OpenContainerTransformDemoState(); +} + +class _OpenContainerTransformDemoState + extends State { + final GlobalKey _scaffoldKey = GlobalKey(); + ContainerTransitionType _transitionType = ContainerTransitionType.fade; + + void _showSettingsBottomModalSheet(BuildContext context) { + final GalleryLocalizations? localizations = GalleryLocalizations.of(context); + + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (BuildContext context, void Function(void Function()) setModalState) { + return Container( + height: 125, + padding: const EdgeInsets.all(15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + localizations!.demoContainerTransformModalBottomSheetTitle, + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox( + height: 12, + ), + ToggleButtons( + borderRadius: BorderRadius.circular(2), + selectedBorderColor: Theme.of(context).colorScheme.primary, + onPressed: (int index) { + setModalState(() { + setState(() { + _transitionType = index == 0 + ? ContainerTransitionType.fade + : ContainerTransitionType.fadeThrough; + }); + }); + }, + isSelected: [ + _transitionType == ContainerTransitionType.fade, + _transitionType == ContainerTransitionType.fadeThrough, + ], + children: [ + Text( + localizations.demoContainerTransformTypeFade, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + ), + child: Text( + localizations.demoContainerTransformTypeFadeThrough, + ), + ), + ], + ), + ], + ), + ); + }, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final GalleryLocalizations? localizations = GalleryLocalizations.of(context); + final ColorScheme colorScheme = Theme.of(context).colorScheme; + + return Navigator( + // Adding [ValueKey] to make sure that the widget gets rebuilt when + // changing type. + key: ValueKey(_transitionType), + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + builder: (BuildContext context) => Scaffold( + key: _scaffoldKey, + appBar: AppBar( + automaticallyImplyLeading: false, + title: Column( + children: [ + Text( + localizations!.demoContainerTransformTitle, + ), + Text( + '(${localizations.demoContainerTransformDemoInstructions})', + style: Theme.of(context) + .textTheme + .titleSmall! + .copyWith(color: Colors.white), + ), + ], + ), + actions: [ + IconButton( + icon: const Icon( + Icons.settings, + ), + onPressed: () { + _showSettingsBottomModalSheet(context); + }, + ), + ], + ), + body: ListView( + padding: const EdgeInsets.all(8), + children: [ + _OpenContainerWrapper( + transitionType: _transitionType, + closedBuilder: (BuildContext context, void Function() openContainer) { + return _DetailsCard(openContainer: openContainer); + }, + ), + const SizedBox(height: 16), + _OpenContainerWrapper( + transitionType: _transitionType, + closedBuilder: (BuildContext context, void Function() openContainer) { + return _DetailsListTile(openContainer: openContainer); + }, + ), + const SizedBox( + height: 16, + ), + Row( + children: [ + Expanded( + child: _OpenContainerWrapper( + transitionType: _transitionType, + closedBuilder: (BuildContext context, void Function() openContainer) { + return _SmallDetailsCard( + openContainer: openContainer, + subtitle: + localizations.demoMotionPlaceholderSubtitle, + ); + }, + ), + ), + const SizedBox( + width: 8, + ), + Expanded( + child: _OpenContainerWrapper( + transitionType: _transitionType, + closedBuilder: (BuildContext context, void Function() openContainer) { + return _SmallDetailsCard( + openContainer: openContainer, + subtitle: + localizations.demoMotionPlaceholderSubtitle, + ); + }, + ), + ), + ], + ), + const SizedBox( + height: 16, + ), + Row( + children: [ + Expanded( + child: _OpenContainerWrapper( + transitionType: _transitionType, + closedBuilder: (BuildContext context, void Function() openContainer) { + return _SmallDetailsCard( + openContainer: openContainer, + subtitle: localizations + .demoMotionSmallPlaceholderSubtitle, + ); + }, + ), + ), + const SizedBox( + width: 8, + ), + Expanded( + child: _OpenContainerWrapper( + transitionType: _transitionType, + closedBuilder: (BuildContext context, void Function() openContainer) { + return _SmallDetailsCard( + openContainer: openContainer, + subtitle: localizations + .demoMotionSmallPlaceholderSubtitle, + ); + }, + ), + ), + const SizedBox( + width: 8, + ), + Expanded( + child: _OpenContainerWrapper( + transitionType: _transitionType, + closedBuilder: (BuildContext context, void Function() openContainer) { + return _SmallDetailsCard( + openContainer: openContainer, + subtitle: localizations + .demoMotionSmallPlaceholderSubtitle, + ); + }, + ), + ), + ], + ), + const SizedBox( + height: 16, + ), + ...List>.generate(10, (int index) { + return OpenContainer( + transitionType: _transitionType, + openBuilder: (BuildContext context, void Function() openContainer) => + const _DetailsPage(), + tappable: false, + closedShape: const RoundedRectangleBorder(), + closedElevation: 0, + closedBuilder: (BuildContext context, void Function() openContainer) { + return ListTile( + leading: Image.asset( + 'placeholders/avatar_logo.png', + package: 'flutter_gallery_assets', + width: 40, + ), + onTap: openContainer, + title: Text( + '${localizations.demoMotionListTileTitle} ${index + 1}', + ), + subtitle: Text( + localizations.demoMotionPlaceholderSubtitle, + ), + ); + }, + ); + }), + ], + ), + floatingActionButton: OpenContainer( + transitionType: _transitionType, + openBuilder: (BuildContext context, void Function() openContainer) => const _DetailsPage(), + closedElevation: 6, + closedShape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(_fabDimension / 2), + ), + ), + closedColor: colorScheme.secondary, + closedBuilder: (BuildContext context, void Function() openContainer) { + return SizedBox( + height: _fabDimension, + width: _fabDimension, + child: Center( + child: Icon( + Icons.add, + color: colorScheme.onSecondary, + ), + ), + ); + }, + ), + ), + ); + }, + ); + } +} + +class _OpenContainerWrapper extends StatelessWidget { + const _OpenContainerWrapper({ + required this.closedBuilder, + required this.transitionType, + }); + + final CloseContainerBuilder closedBuilder; + final ContainerTransitionType transitionType; + + @override + Widget build(BuildContext context) { + return OpenContainer( + transitionType: transitionType, + openBuilder: (BuildContext context, void Function() openContainer) => const _DetailsPage(), + tappable: false, + closedBuilder: closedBuilder, + ); + } +} + +class _DetailsCard extends StatelessWidget { + const _DetailsCard({required this.openContainer}); + + final VoidCallback openContainer; + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + return _InkWellOverlay( + openContainer: openContainer, + height: 300, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: ColoredBox( + color: Colors.black38, + child: Center( + child: Image.asset( + 'placeholders/placeholder_image.png', + package: 'flutter_gallery_assets', + width: 100, + ), + ), + ), + ), + ListTile( + title: Text( + localizations.demoMotionPlaceholderTitle, + ), + subtitle: Text( + localizations.demoMotionPlaceholderSubtitle, + ), + ), + Padding( + padding: const EdgeInsets.only( + left: 16, + right: 16, + bottom: 16, + ), + child: Text( + 'Lorem ipsum dolor sit amet, consectetur ' + 'adipiscing elit, sed do eiusmod tempor.', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Colors.black54, + inherit: false, + ), + ), + ), + ], + ), + ); + } +} + +class _SmallDetailsCard extends StatelessWidget { + const _SmallDetailsCard({ + required this.openContainer, + required this.subtitle, + }); + + final VoidCallback openContainer; + final String subtitle; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + + return _InkWellOverlay( + openContainer: openContainer, + height: 225, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + color: Colors.black38, + height: 150, + child: Center( + child: Image.asset( + 'placeholders/placeholder_image.png', + package: 'flutter_gallery_assets', + width: 80, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + GalleryLocalizations.of(context)! + .demoMotionPlaceholderTitle, + style: textTheme.titleLarge, + ), + const SizedBox( + height: 4, + ), + Text( + subtitle, + style: textTheme.bodySmall, + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _DetailsListTile extends StatelessWidget { + const _DetailsListTile({required this.openContainer}); + + final VoidCallback openContainer; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + const double height = 120.0; + + return _InkWellOverlay( + openContainer: openContainer, + height: height, + child: Row( + children: [ + Container( + color: Colors.black38, + height: height, + width: height, + child: Center( + child: Image.asset( + 'placeholders/placeholder_image.png', + package: 'flutter_gallery_assets', + width: 60, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + GalleryLocalizations.of(context)! + .demoMotionPlaceholderTitle, + style: textTheme.titleMedium, + ), + const SizedBox( + height: 8, + ), + Text( + 'Lorem ipsum dolor sit amet, consectetur ' + 'adipiscing elit,', + style: textTheme.bodySmall, + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _InkWellOverlay extends StatelessWidget { + const _InkWellOverlay({ + required this.openContainer, + required this.height, + required this.child, + }); + + final VoidCallback openContainer; + final double height; + final Widget child; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: height, + child: InkWell( + onTap: openContainer, + child: child, + ), + ); + } +} + +class _DetailsPage extends StatelessWidget { + const _DetailsPage(); + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + final TextTheme textTheme = Theme.of(context).textTheme; + + return Scaffold( + appBar: AppBar( + title: Text( + localizations.demoMotionDetailsPageTitle, + ), + ), + body: ListView( + children: [ + Container( + color: Colors.black38, + height: 250, + child: Padding( + padding: const EdgeInsets.all(70), + child: Image.asset( + 'placeholders/placeholder_image.png', + package: 'flutter_gallery_assets', + ), + ), + ), + Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + localizations.demoMotionPlaceholderTitle, + style: textTheme.headlineSmall!.copyWith( + color: Colors.black54, + fontSize: 30, + ), + ), + const SizedBox( + height: 10, + ), + Text( + _loremIpsumParagraph, + style: textTheme.bodyMedium!.copyWith( + color: Colors.black54, + height: 1.5, + fontSize: 16, + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +// END openContainerTransformDemo diff --git a/dev/integration_tests/new_gallery/lib/demos/reference/motion_demo_fade_scale_transition.dart b/dev/integration_tests/new_gallery/lib/demos/reference/motion_demo_fade_scale_transition.dart new file mode 100644 index 0000000000..919d948424 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/reference/motion_demo_fade_scale_transition.dart @@ -0,0 +1,166 @@ +// 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 '../../gallery_localizations.dart'; + +// BEGIN fadeScaleTransitionDemo + +class FadeScaleTransitionDemo extends StatefulWidget { + const FadeScaleTransitionDemo({super.key}); + + @override + State createState() => + _FadeScaleTransitionDemoState(); +} + +class _FadeScaleTransitionDemoState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + value: 0, + duration: const Duration(milliseconds: 150), + reverseDuration: const Duration(milliseconds: 75), + vsync: this, + )..addStatusListener((AnimationStatus status) { + setState(() { + // setState needs to be called to trigger a rebuild because + // the 'HIDE FAB'/'SHOW FAB' button needs to be updated based + // the latest value of [_controller.status]. + }); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + bool get _isAnimationRunningForwardsOrComplete { + switch (_controller.status) { + case AnimationStatus.forward: + case AnimationStatus.completed: + return true; + case AnimationStatus.reverse: + case AnimationStatus.dismissed: + return false; + } + } + + Widget _showExampleAlertDialog() { + return Theme( + data: Theme.of(context), + child: _ExampleAlertDialog(), + ); + } + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Column( + children: [ + Text(localizations.demoFadeScaleTitle), + Text( + '(${localizations.demoFadeScaleDemoInstructions})', + style: Theme.of(context) + .textTheme + .titleSmall! + .copyWith(color: Colors.white), + ), + ], + ), + ), + floatingActionButton: AnimatedBuilder( + animation: _controller, + builder: (BuildContext context, Widget? child) { + return FadeScaleTransition( + animation: _controller, + child: child, + ); + }, + child: Visibility( + visible: _controller.status != AnimationStatus.dismissed, + child: FloatingActionButton( + onPressed: () {}, + child: const Icon(Icons.add), + ), + ), + ), + bottomNavigationBar: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Divider(height: 0), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + showModal( + context: context, + builder: (BuildContext context) => _showExampleAlertDialog()); + }, + child: Text(localizations.demoFadeScaleShowAlertDialogButton), + ), + const SizedBox(width: 10), + ElevatedButton( + onPressed: () { + if (_isAnimationRunningForwardsOrComplete) { + _controller.reverse(); + } else { + _controller.forward(); + } + }, + child: Text( + _isAnimationRunningForwardsOrComplete + ? localizations.demoFadeScaleHideFabButton + : localizations.demoFadeScaleShowFabButton, + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _ExampleAlertDialog extends StatelessWidget { + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + return AlertDialog( + content: Text(localizations.demoFadeScaleAlertDialogHeader), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(localizations.demoFadeScaleAlertDialogCancelButton), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(localizations.demoFadeScaleAlertDialogDiscardButton), + ), + ], + ); + } +} + +// END fadeScaleTransitionDemo diff --git a/dev/integration_tests/new_gallery/lib/demos/reference/motion_demo_fade_through_transition.dart b/dev/integration_tests/new_gallery/lib/demos/reference/motion_demo_fade_through_transition.dart new file mode 100644 index 0000000000..875f356436 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/reference/motion_demo_fade_through_transition.dart @@ -0,0 +1,200 @@ +// 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 '../../gallery_localizations.dart'; + +// BEGIN fadeThroughTransitionDemo + +class FadeThroughTransitionDemo extends StatefulWidget { + const FadeThroughTransitionDemo({super.key}); + + @override + State createState() => + _FadeThroughTransitionDemoState(); +} + +class _FadeThroughTransitionDemoState extends State { + int _pageIndex = 0; + + final List _pageList = [ + _AlbumsPage(), + _PhotosPage(), + _SearchPage(), + ]; + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Column( + children: [ + Text(localizations.demoFadeThroughTitle), + Text( + '(${localizations.demoFadeThroughDemoInstructions})', + style: Theme.of(context) + .textTheme + .titleSmall! + .copyWith(color: Colors.white), + ), + ], + ), + ), + body: PageTransitionSwitcher( + transitionBuilder: ( + Widget child, + Animation animation, + Animation secondaryAnimation, + ) { + return FadeThroughTransition( + animation: animation, + secondaryAnimation: secondaryAnimation, + child: child, + ); + }, + child: _pageList[_pageIndex], + ), + bottomNavigationBar: BottomNavigationBar( + currentIndex: _pageIndex, + onTap: (int selectedIndex) { + setState(() { + _pageIndex = selectedIndex; + }); + }, + items: [ + BottomNavigationBarItem( + icon: const Icon(Icons.photo_library), + label: localizations.demoFadeThroughAlbumsDestination, + ), + BottomNavigationBarItem( + icon: const Icon(Icons.photo), + label: localizations.demoFadeThroughPhotosDestination, + ), + BottomNavigationBarItem( + icon: const Icon(Icons.search), + label: localizations.demoFadeThroughSearchDestination, + ), + ], + ), + ); + } +} + +class _ExampleCard extends StatelessWidget { + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + final TextTheme textTheme = Theme.of(context).textTheme; + + return Expanded( + child: Card( + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: ColoredBox( + color: Colors.black26, + child: Padding( + padding: const EdgeInsets.all(30), + child: Ink.image( + image: const AssetImage( + 'placeholders/placeholder_image.png', + package: 'flutter_gallery_assets', + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + localizations.demoFadeThroughTextPlaceholder, + style: textTheme.bodyLarge, + ), + Text( + localizations.demoFadeThroughTextPlaceholder, + style: textTheme.bodySmall, + ), + ], + ), + ), + ], + ), + InkWell( + splashColor: Colors.black38, + onTap: () {}, + ), + ], + ), + ), + ); + } +} + +class _AlbumsPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Column( + children: [ + ...List.generate( + 3, + (int index) => Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _ExampleCard(), + _ExampleCard(), + ], + ), + ), + ), + ], + ); + } +} + +class _PhotosPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Column( + children: [ + _ExampleCard(), + _ExampleCard(), + ], + ); + } +} + +class _SearchPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + final GalleryLocalizations? localizations = GalleryLocalizations.of(context); + + return ListView.builder( + itemBuilder: (BuildContext context, int index) { + return ListTile( + leading: Image.asset( + 'placeholders/avatar_logo.png', + package: 'flutter_gallery_assets', + width: 40, + ), + title: Text('${localizations!.demoMotionListTileTitle} ${index + 1}'), + subtitle: Text(localizations.demoMotionPlaceholderSubtitle), + ); + }, + itemCount: 10, + ); + } +} + +// END fadeThroughTransitionDemo diff --git a/dev/integration_tests/new_gallery/lib/demos/reference/motion_demo_shared_x_axis_transition.dart b/dev/integration_tests/new_gallery/lib/demos/reference/motion_demo_shared_x_axis_transition.dart new file mode 100644 index 0000000000..22fce43481 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/reference/motion_demo_shared_x_axis_transition.dart @@ -0,0 +1,247 @@ +// 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 '../../gallery_localizations.dart'; + +// BEGIN sharedXAxisTransitionDemo + +class SharedXAxisTransitionDemo extends StatefulWidget { + const SharedXAxisTransitionDemo({super.key}); + @override + State createState() => + _SharedXAxisTransitionDemoState(); +} + +class _SharedXAxisTransitionDemoState extends State { + bool _isLoggedIn = false; + + void _toggleLoginStatus() { + setState(() { + _isLoggedIn = !_isLoggedIn; + }); + } + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + return Scaffold( + resizeToAvoidBottomInset: false, + appBar: AppBar( + automaticallyImplyLeading: false, + title: Column( + children: [ + Text(localizations.demoSharedXAxisTitle), + Text( + '(${localizations.demoSharedXAxisDemoInstructions})', + style: Theme.of(context) + .textTheme + .titleSmall! + .copyWith(color: Colors.white), + ), + ], + ), + ), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: PageTransitionSwitcher( + reverse: !_isLoggedIn, + transitionBuilder: ( + Widget child, + Animation animation, + Animation secondaryAnimation, + ) { + return SharedAxisTransition( + animation: animation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.horizontal, + child: child, + ); + }, + child: _isLoggedIn ? const _CoursePage() : const _SignInPage(), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: _isLoggedIn ? _toggleLoginStatus : null, + child: Text(localizations.demoSharedXAxisBackButtonText), + ), + ElevatedButton( + onPressed: _isLoggedIn ? null : _toggleLoginStatus, + child: Text(localizations.demoSharedXAxisNextButtonText), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class _CoursePage extends StatelessWidget { + const _CoursePage(); + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + return ListView( + children: [ + const SizedBox(height: 16), + Text( + localizations.demoSharedXAxisCoursePageTitle, + style: Theme.of(context).textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Text( + localizations.demoSharedXAxisCoursePageSubtitle, + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + textAlign: TextAlign.center, + ), + ), + _CourseSwitch( + course: localizations.demoSharedXAxisArtsAndCraftsCourseTitle), + _CourseSwitch(course: localizations.demoSharedXAxisBusinessCourseTitle), + _CourseSwitch( + course: localizations.demoSharedXAxisIllustrationCourseTitle), + _CourseSwitch(course: localizations.demoSharedXAxisDesignCourseTitle), + _CourseSwitch(course: localizations.demoSharedXAxisCulinaryCourseTitle), + ], + ); + } +} + +class _CourseSwitch extends StatefulWidget { + const _CourseSwitch({ + this.course, + }); + + final String? course; + + @override + _CourseSwitchState createState() => _CourseSwitchState(); +} + +class _CourseSwitchState extends State<_CourseSwitch> { + bool _isCourseBundled = true; + + @override + Widget build(BuildContext context) { + final GalleryLocalizations? localizations = GalleryLocalizations.of(context); + final String subtitle = _isCourseBundled + ? localizations!.demoSharedXAxisBundledCourseSubtitle + : localizations!.demoSharedXAxisIndividualCourseSubtitle; + + return SwitchListTile( + title: Text(widget.course!), + subtitle: Text(subtitle), + value: _isCourseBundled, + onChanged: (bool newValue) { + setState(() { + _isCourseBundled = newValue; + }); + }, + ); + } +} + +class _SignInPage extends StatelessWidget { + const _SignInPage(); + + @override + Widget build(BuildContext context) { + final GalleryLocalizations? localizations = GalleryLocalizations.of(context); + + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + final double maxHeight = constraints.maxHeight; + const SizedBox spacing = SizedBox(height: 10); + + return Container( + constraints: const BoxConstraints(maxWidth: 400), + child: Column( + children: [ + SizedBox(height: maxHeight / 10), + Image.asset( + 'placeholders/avatar_logo.png', + package: 'flutter_gallery_assets', + width: 80, + height: 80, + ), + spacing, + Text( + localizations!.demoSharedXAxisSignInWelcomeText, + style: Theme.of(context).textTheme.headlineSmall, + ), + spacing, + Text( + localizations.demoSharedXAxisSignInSubtitleText, + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsetsDirectional.only( + top: 40, + start: 10, + end: 10, + bottom: 10, + ), + child: TextField( + decoration: InputDecoration( + suffixIcon: const Icon( + Icons.visibility, + size: 20, + color: Colors.black54, + ), + labelText: + localizations.demoSharedXAxisSignInTextFieldLabel, + border: const OutlineInputBorder(), + ), + ), + ), + TextButton( + onPressed: () {}, + child: Text( + localizations.demoSharedXAxisForgotEmailButtonText, + ), + ), + spacing, + TextButton( + onPressed: () {}, + child: Text( + localizations.demoSharedXAxisCreateAccountButtonText, + ), + ), + ], + ), + ], + ), + ); + }, + ); + } +} + +// END sharedXAxisTransitionDemo diff --git a/dev/integration_tests/new_gallery/lib/demos/reference/motion_demo_shared_y_axis_transition.dart b/dev/integration_tests/new_gallery/lib/demos/reference/motion_demo_shared_y_axis_transition.dart new file mode 100644 index 0000000000..625c014131 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/reference/motion_demo_shared_y_axis_transition.dart @@ -0,0 +1,201 @@ +// 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 'dart:math'; +import 'package:animations/animations.dart'; +import 'package:flutter/material.dart'; +import '../../gallery_localizations.dart'; + +// BEGIN sharedYAxisTransitionDemo + +class SharedYAxisTransitionDemo extends StatefulWidget { + const SharedYAxisTransitionDemo({super.key}); + + @override + State createState() => + _SharedYAxisTransitionDemoState(); +} + +class _SharedYAxisTransitionDemoState extends State + with SingleTickerProviderStateMixin { + bool _isAlphabetical = false; + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + } + + final ListView _recentList = ListView( + // Adding [UniqueKey] to make sure the widget rebuilds when transitioning. + key: UniqueKey(), + children: [ + for (int i = 0; i < 10; i++) _AlbumTile((i + 1).toString()), + ], + ); + + final ListView _alphabeticalList = ListView( + // Adding [UniqueKey] to make sure the widget rebuilds when transitioning. + key: UniqueKey(), + children: [ + for (final String letter in _alphabet) _AlbumTile(letter), + ], + ); + + static const List _alphabet = [ + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + 'G', + 'H', + 'I', + 'J', + ]; + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Column( + children: [ + Text(localizations.demoSharedYAxisTitle), + Text( + '(${localizations.demoSharedYAxisDemoInstructions})', + style: Theme.of(context) + .textTheme + .titleSmall! + .copyWith(color: Colors.white), + ), + ], + ), + ), + body: Column( + children: [ + const SizedBox(height: 5), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 15), + child: Text(localizations.demoSharedYAxisAlbumCount), + ), + Padding( + padding: const EdgeInsets.only(right: 7), + child: InkWell( + customBorder: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(4), + ), + ), + onTap: () { + if (!_isAlphabetical) { + _controller.reset(); + _controller.animateTo(0.5); + } else { + _controller.animateTo(1); + } + setState(() { + _isAlphabetical = !_isAlphabetical; + }); + }, + child: Row( + children: [ + Text(_isAlphabetical + ? localizations.demoSharedYAxisAlphabeticalSortTitle + : localizations.demoSharedYAxisRecentSortTitle), + RotationTransition( + turns: Tween(begin: 0.0, end: 1.0) + .animate(_controller.view), + child: const Icon(Icons.arrow_drop_down), + ), + ], + ), + ), + ), + ], + ), + const SizedBox(height: 10), + Expanded( + child: PageTransitionSwitcher( + reverse: _isAlphabetical, + transitionBuilder: (Widget child, Animation animation, Animation secondaryAnimation) { + return SharedAxisTransition( + animation: animation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.vertical, + child: child, + ); + }, + child: _isAlphabetical ? _alphabeticalList : _recentList, + ), + ), + ], + ), + ); + } +} + +class _AlbumTile extends StatelessWidget { + const _AlbumTile(this._title); + final String _title; + + @override + Widget build(BuildContext context) { + final Random randomNumberGenerator = Random(); + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + return Column( + children: [ + ListTile( + leading: Container( + height: 60, + width: 60, + decoration: const BoxDecoration( + borderRadius: BorderRadius.all( + Radius.circular(4), + ), + color: Colors.grey, + ), + child: Padding( + padding: const EdgeInsets.all(6), + child: Image.asset( + 'placeholders/placeholder_image.png', + package: 'flutter_gallery_assets', + ), + ), + ), + title: Text( + '${localizations.demoSharedYAxisAlbumTileTitle} $_title', + ), + subtitle: Text( + localizations.demoSharedYAxisAlbumTileSubtitle, + ), + trailing: Text( + '${randomNumberGenerator.nextInt(50) + 10} ' + '${localizations.demoSharedYAxisAlbumTileDurationUnit}', + ), + ), + const Divider(height: 20, thickness: 1), + ], + ); + } +} + +// END sharedYAxisTransitionDemo diff --git a/dev/integration_tests/new_gallery/lib/demos/reference/motion_demo_shared_z_axis_transition.dart b/dev/integration_tests/new_gallery/lib/demos/reference/motion_demo_shared_z_axis_transition.dart new file mode 100644 index 0000000000..f64e2b5f02 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/reference/motion_demo_shared_z_axis_transition.dart @@ -0,0 +1,260 @@ +// 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 '../../gallery_localizations.dart'; + +// BEGIN sharedZAxisTransitionDemo + +class SharedZAxisTransitionDemo extends StatelessWidget { + const SharedZAxisTransitionDemo({super.key}); + + @override + Widget build(BuildContext context) { + return Navigator( + onGenerateRoute: (RouteSettings settings) { + return _createHomeRoute(); + }, + ); + } + + Route _createHomeRoute() { + return PageRouteBuilder( + pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Column( + children: [ + Text(localizations.demoSharedZAxisTitle), + Text( + '(${localizations.demoSharedZAxisDemoInstructions})', + style: Theme.of(context) + .textTheme + .titleSmall! + .copyWith(color: Colors.white), + ), + ], + ), + actions: [ + IconButton( + icon: const Icon(Icons.settings), + onPressed: () { + Navigator.of(context).push(_createSettingsRoute()); + }, + ), + ], + ), + body: const _RecipePage(), + ); + }, + transitionsBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { + return SharedAxisTransition( + fillColor: Colors.transparent, + transitionType: SharedAxisTransitionType.scaled, + animation: animation, + secondaryAnimation: secondaryAnimation, + child: child, + ); + }, + ); + } + + Route _createSettingsRoute() { + return PageRouteBuilder( + pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) => + const _SettingsPage(), + transitionsBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { + return SharedAxisTransition( + fillColor: Colors.transparent, + transitionType: SharedAxisTransitionType.scaled, + animation: animation, + secondaryAnimation: secondaryAnimation, + child: child, + ); + }, + ); + } +} + +class _SettingsPage extends StatelessWidget { + const _SettingsPage(); + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + final List<_SettingsInfo> settingsList = <_SettingsInfo>[ + _SettingsInfo( + Icons.person, + localizations.demoSharedZAxisProfileSettingLabel, + ), + _SettingsInfo( + Icons.notifications, + localizations.demoSharedZAxisNotificationSettingLabel, + ), + _SettingsInfo( + Icons.security, + localizations.demoSharedZAxisPrivacySettingLabel, + ), + _SettingsInfo( + Icons.help, + localizations.demoSharedZAxisHelpSettingLabel, + ), + ]; + + return Scaffold( + appBar: AppBar( + title: Text( + localizations.demoSharedZAxisSettingsPageTitle, + ), + ), + body: ListView( + children: [ + for (final _SettingsInfo setting in settingsList) _SettingsTile(setting), + ], + ), + ); + } +} + +class _SettingsTile extends StatelessWidget { + const _SettingsTile(this.settingData); + final _SettingsInfo settingData; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + ListTile( + leading: Icon(settingData.settingIcon), + title: Text(settingData.settingsLabel), + ), + const Divider(thickness: 2), + ], + ); + } +} + +class _SettingsInfo { + const _SettingsInfo(this.settingIcon, this.settingsLabel); + + final IconData settingIcon; + final String settingsLabel; +} + +class _RecipePage extends StatelessWidget { + const _RecipePage(); + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + final List<_RecipeInfo> savedRecipes = <_RecipeInfo>[ + _RecipeInfo( + localizations.demoSharedZAxisBurgerRecipeTitle, + localizations.demoSharedZAxisBurgerRecipeDescription, + 'crane/destinations/eat_2.jpg', + ), + _RecipeInfo( + localizations.demoSharedZAxisSandwichRecipeTitle, + localizations.demoSharedZAxisSandwichRecipeDescription, + 'crane/destinations/eat_3.jpg', + ), + _RecipeInfo( + localizations.demoSharedZAxisDessertRecipeTitle, + localizations.demoSharedZAxisDessertRecipeDescription, + 'crane/destinations/eat_4.jpg', + ), + _RecipeInfo( + localizations.demoSharedZAxisShrimpPlateRecipeTitle, + localizations.demoSharedZAxisShrimpPlateRecipeDescription, + 'crane/destinations/eat_6.jpg', + ), + _RecipeInfo( + localizations.demoSharedZAxisCrabPlateRecipeTitle, + localizations.demoSharedZAxisCrabPlateRecipeDescription, + 'crane/destinations/eat_8.jpg', + ), + _RecipeInfo( + localizations.demoSharedZAxisBeefSandwichRecipeTitle, + localizations.demoSharedZAxisBeefSandwichRecipeDescription, + 'crane/destinations/eat_10.jpg', + ), + ]; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8), + Padding( + padding: const EdgeInsetsDirectional.only(start: 8.0), + child: Text(localizations.demoSharedZAxisSavedRecipesListTitle), + ), + const SizedBox(height: 4), + Expanded( + child: ListView( + padding: const EdgeInsets.all(8), + children: [ + for (final _RecipeInfo recipe in savedRecipes) + _RecipeTile(recipe, savedRecipes.indexOf(recipe)) + ], + ), + ), + ], + ); + } +} + +class _RecipeInfo { + const _RecipeInfo(this.recipeName, this.recipeDescription, this.recipeImage); + + final String recipeName; + final String recipeDescription; + final String recipeImage; +} + +class _RecipeTile extends StatelessWidget { + const _RecipeTile(this._recipe, this._index); + final _RecipeInfo _recipe; + final int _index; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + SizedBox( + height: 70, + width: 100, + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(4)), + child: Image.asset( + _recipe.recipeImage, + package: 'flutter_gallery_assets', + fit: BoxFit.fill, + ), + ), + ), + const SizedBox(width: 24), + Expanded( + child: Column( + children: [ + ListTile( + title: Text(_recipe.recipeName), + subtitle: Text(_recipe.recipeDescription), + trailing: Text('0${_index + 1}'), + ), + const Divider(thickness: 2), + ], + ), + ), + ], + ); + } +} + +// END sharedZAxisTransitionDemo diff --git a/dev/integration_tests/new_gallery/lib/demos/reference/transformations_demo.dart b/dev/integration_tests/new_gallery/lib/demos/reference/transformations_demo.dart new file mode 100644 index 0000000000..14d0ee45e7 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/reference/transformations_demo.dart @@ -0,0 +1,247 @@ +// 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 'dart:ui'; + +import 'package:flutter/material.dart'; +import '../../gallery_localizations.dart'; + +import 'transformations_demo_board.dart'; +import 'transformations_demo_edit_board_point.dart'; + +// BEGIN transformationsDemo#1 + +class TransformationsDemo extends StatefulWidget { + const TransformationsDemo({super.key}); + + @override + State createState() => _TransformationsDemoState(); +} + +class _TransformationsDemoState extends State + with TickerProviderStateMixin { + final GlobalKey _targetKey = GlobalKey(); + // The radius of a hexagon tile in pixels. + static const double _kHexagonRadius = 16.0; + // The margin between hexagons. + static const double _kHexagonMargin = 1.0; + // The radius of the entire board in hexagons, not including the center. + static const int _kBoardRadius = 8; + + Board _board = Board( + boardRadius: _kBoardRadius, + hexagonRadius: _kHexagonRadius, + hexagonMargin: _kHexagonMargin, + ); + + final TransformationController _transformationController = + TransformationController(); + Animation? _animationReset; + late AnimationController _controllerReset; + Matrix4? _homeMatrix; + + // Handle reset to home transform animation. + void _onAnimateReset() { + _transformationController.value = _animationReset!.value; + if (!_controllerReset.isAnimating) { + _animationReset?.removeListener(_onAnimateReset); + _animationReset = null; + _controllerReset.reset(); + } + } + + // Initialize the reset to home transform animation. + void _animateResetInitialize() { + _controllerReset.reset(); + _animationReset = Matrix4Tween( + begin: _transformationController.value, + end: _homeMatrix, + ).animate(_controllerReset); + _controllerReset.duration = const Duration(milliseconds: 400); + _animationReset!.addListener(_onAnimateReset); + _controllerReset.forward(); + } + + // Stop a running reset to home transform animation. + void _animateResetStop() { + _controllerReset.stop(); + _animationReset?.removeListener(_onAnimateReset); + _animationReset = null; + _controllerReset.reset(); + } + + void _onScaleStart(ScaleStartDetails details) { + // If the user tries to cause a transformation while the reset animation is + // running, cancel the reset animation. + if (_controllerReset.status == AnimationStatus.forward) { + _animateResetStop(); + } + } + + void _onTapUp(TapUpDetails details) { + final RenderBox renderBox = + _targetKey.currentContext!.findRenderObject()! as RenderBox; + final Offset offset = + details.globalPosition - renderBox.localToGlobal(Offset.zero); + final Offset scenePoint = _transformationController.toScene(offset); + final BoardPoint? boardPoint = _board.pointToBoardPoint(scenePoint); + setState(() { + _board = _board.copyWithSelected(boardPoint); + }); + } + + @override + void initState() { + super.initState(); + _controllerReset = AnimationController( + vsync: this, + ); + } + + @override + Widget build(BuildContext context) { + // The scene is drawn by a CustomPaint, but user interaction is handled by + // the InteractiveViewer parent widget. + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.primary, + appBar: AppBar( + automaticallyImplyLeading: false, + title: + Text(GalleryLocalizations.of(context)!.demo2dTransformationsTitle), + ), + body: ColoredBox( + color: backgroundColor, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + // Draw the scene as big as is available, but allow the user to + // translate beyond that to a visibleSize that's a bit bigger. + final Size viewportSize = Size( + constraints.maxWidth, + constraints.maxHeight, + ); + + // Start the first render, start the scene centered in the viewport. + if (_homeMatrix == null) { + _homeMatrix = Matrix4.identity() + ..translate( + viewportSize.width / 2 - _board.size.width / 2, + viewportSize.height / 2 - _board.size.height / 2, + ); + _transformationController.value = _homeMatrix!; + } + + return ClipRect( + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTapUp: _onTapUp, + child: InteractiveViewer( + key: _targetKey, + transformationController: _transformationController, + boundaryMargin: EdgeInsets.symmetric( + horizontal: viewportSize.width, + vertical: viewportSize.height, + ), + minScale: 0.01, + onInteractionStart: _onScaleStart, + child: SizedBox.expand( + child: CustomPaint( + size: _board.size, + painter: _BoardPainter( + board: _board, + ), + ), + ), + ), + ), + ), + ); + }, + ), + ), + persistentFooterButtons: [resetButton, editButton], + ); + } + + IconButton get resetButton { + return IconButton( + onPressed: () { + setState(() { + _animateResetInitialize(); + }); + }, + tooltip: 'Reset', + color: Theme.of(context).colorScheme.surface, + icon: const Icon(Icons.replay), + ); + } + + IconButton get editButton { + return IconButton( + onPressed: () { + if (_board.selected == null) { + return; + } + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return Container( + width: double.infinity, + height: 150, + padding: const EdgeInsets.all(12), + child: EditBoardPoint( + boardPoint: _board.selected!, + onColorSelection: (Color color) { + setState(() { + _board = _board.copyWithBoardPointColor( + _board.selected!, color); + Navigator.pop(context); + }); + }, + ), + ); + }); + }, + tooltip: 'Edit', + color: Theme.of(context).colorScheme.surface, + icon: const Icon(Icons.edit), + ); + } + + @override + void dispose() { + _controllerReset.dispose(); + super.dispose(); + } +} + +// CustomPainter is what is passed to CustomPaint and actually draws the scene +// when its `paint` method is called. +class _BoardPainter extends CustomPainter { + const _BoardPainter({required this.board}); + + final Board board; + + @override + void paint(Canvas canvas, Size size) { + void drawBoardPoint(BoardPoint? boardPoint) { + final Color color = boardPoint!.color.withOpacity( + board.selected == boardPoint ? 0.7 : 1, + ); + final Vertices vertices = board.getVerticesForBoardPoint(boardPoint, color); + canvas.drawVertices(vertices, BlendMode.color, Paint()); + } + + board.forEach(drawBoardPoint); + } + + // We should repaint whenever the board changes, such as board.selected. + @override + bool shouldRepaint(_BoardPainter oldDelegate) { + return oldDelegate.board != board; + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/reference/transformations_demo_board.dart b/dev/integration_tests/new_gallery/lib/demos/reference/transformations_demo_board.dart new file mode 100644 index 0000000000..176232fa7c --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/reference/transformations_demo_board.dart @@ -0,0 +1,301 @@ +// 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 'dart:collection' show IterableMixin; +import 'dart:math'; +import 'dart:ui' show Vertices; +import 'package:flutter/material.dart'; +import 'package:vector_math/vector_math_64.dart' show Vector3; + +// BEGIN transformationsDemo#2 + +// The entire state of the hex board and abstraction to get information about +// it. Iterable so that all BoardPoints on the board can be iterated over. +@immutable +class Board extends Object with IterableMixin { + Board({ + required this.boardRadius, + required this.hexagonRadius, + required this.hexagonMargin, + this.selected, + List? boardPoints, + }) : assert(boardRadius > 0), + assert(hexagonRadius > 0), + assert(hexagonMargin >= 0) { + // Set up the positions for the center hexagon where the entire board is + // centered on the origin. + // Start point of hexagon (top vertex). + final Point hexStart = Point(0, -hexagonRadius); + final double hexagonRadiusPadded = hexagonRadius - hexagonMargin; + final double centerToFlat = sqrt(3) / 2 * hexagonRadiusPadded; + positionsForHexagonAtOrigin.addAll([ + Offset(hexStart.x, hexStart.y), + Offset(hexStart.x + centerToFlat, hexStart.y + 0.5 * hexagonRadiusPadded), + Offset(hexStart.x + centerToFlat, hexStart.y + 1.5 * hexagonRadiusPadded), + Offset(hexStart.x + centerToFlat, hexStart.y + 1.5 * hexagonRadiusPadded), + Offset(hexStart.x, hexStart.y + 2 * hexagonRadiusPadded), + Offset(hexStart.x, hexStart.y + 2 * hexagonRadiusPadded), + Offset(hexStart.x - centerToFlat, hexStart.y + 1.5 * hexagonRadiusPadded), + Offset(hexStart.x - centerToFlat, hexStart.y + 1.5 * hexagonRadiusPadded), + Offset(hexStart.x - centerToFlat, hexStart.y + 0.5 * hexagonRadiusPadded), + ]); + + if (boardPoints != null) { + _boardPoints.addAll(boardPoints); + } else { + // Generate boardPoints for a fresh board. + BoardPoint? boardPoint = _getNextBoardPoint(null); + while (boardPoint != null) { + _boardPoints.add(boardPoint); + boardPoint = _getNextBoardPoint(boardPoint); + } + } + } + + final int boardRadius; // Number of hexagons from center to edge. + final double hexagonRadius; // Pixel radius of a hexagon (center to vertex). + final double hexagonMargin; // Margin between hexagons. + final List positionsForHexagonAtOrigin = []; + final BoardPoint? selected; + final List _boardPoints = []; + + @override + Iterator get iterator => _BoardIterator(_boardPoints); + + // For a given q axial coordinate, get the range of possible r values + // See the definition of BoardPoint for more information about hex grids and + // axial coordinates. + _Range _getRRangeForQ(int q) { + int rStart; + int rEnd; + if (q <= 0) { + rStart = -boardRadius - q; + rEnd = boardRadius; + } else { + rEnd = boardRadius - q; + rStart = -boardRadius; + } + + return _Range(rStart, rEnd); + } + + // Get the BoardPoint that comes after the given BoardPoint. If given null, + // returns the origin BoardPoint. If given BoardPoint is the last, returns + // null. + BoardPoint? _getNextBoardPoint(BoardPoint? boardPoint) { + // If before the first element. + if (boardPoint == null) { + return BoardPoint(-boardRadius, 0); + } + + final _Range rRange = _getRRangeForQ(boardPoint.q); + + // If at or after the last element. + if (boardPoint.q >= boardRadius && boardPoint.r >= rRange.max) { + return null; + } + + // If wrapping from one q to the next. + if (boardPoint.r >= rRange.max) { + return BoardPoint(boardPoint.q + 1, _getRRangeForQ(boardPoint.q + 1).min); + } + + // Otherwise we're just incrementing r. + return BoardPoint(boardPoint.q, boardPoint.r + 1); + } + + // Check if the board point is actually on the board. + bool _validateBoardPoint(BoardPoint boardPoint) { + const BoardPoint center = BoardPoint(0, 0); + final int distanceFromCenter = getDistance(center, boardPoint); + return distanceFromCenter <= boardRadius; + } + + // Get the size in pixels of the entire board. + Size get size { + final double centerToFlat = sqrt(3) / 2 * hexagonRadius; + return Size( + (boardRadius * 2 + 1) * centerToFlat * 2, + 2 * (hexagonRadius + boardRadius * 1.5 * hexagonRadius), + ); + } + + // Get the distance between two BoardPoints. + static int getDistance(BoardPoint a, BoardPoint b) { + final Vector3 a3 = a.cubeCoordinates; + final Vector3 b3 = b.cubeCoordinates; + return ((a3.x - b3.x).abs() + (a3.y - b3.y).abs() + (a3.z - b3.z).abs()) ~/ + 2; + } + + // Return the q,r BoardPoint for a point in the scene, where the origin is in + // the center of the board in both coordinate systems. If no BoardPoint at the + // location, return null. + BoardPoint? pointToBoardPoint(Offset point) { + final Offset pointCentered = Offset( + point.dx - size.width / 2, + point.dy - size.height / 2, + ); + final BoardPoint boardPoint = BoardPoint( + ((sqrt(3) / 3 * pointCentered.dx - 1 / 3 * pointCentered.dy) / + hexagonRadius) + .round(), + ((2 / 3 * pointCentered.dy) / hexagonRadius).round(), + ); + + if (!_validateBoardPoint(boardPoint)) { + return null; + } + + return _boardPoints.firstWhere((BoardPoint boardPointI) { + return boardPointI.q == boardPoint.q && boardPointI.r == boardPoint.r; + }); + } + + // Return a scene point for the center of a hexagon given its q,r point. + Point boardPointToPoint(BoardPoint boardPoint) { + return Point( + sqrt(3) * hexagonRadius * boardPoint.q + + sqrt(3) / 2 * hexagonRadius * boardPoint.r + + size.width / 2, + 1.5 * hexagonRadius * boardPoint.r + size.height / 2, + ); + } + + // Get Vertices that can be drawn to a Canvas for the given BoardPoint. + Vertices getVerticesForBoardPoint(BoardPoint boardPoint, Color color) { + final Point centerOfHexZeroCenter = boardPointToPoint(boardPoint); + + final List positions = positionsForHexagonAtOrigin.map((Offset offset) { + return offset.translate(centerOfHexZeroCenter.x, centerOfHexZeroCenter.y); + }).toList(); + + return Vertices( + VertexMode.triangleFan, + positions, + colors: List.filled(positions.length, color), + ); + } + + // Return a new board with the given BoardPoint selected. + Board copyWithSelected(BoardPoint? boardPoint) { + if (selected == boardPoint) { + return this; + } + final Board nextBoard = Board( + boardRadius: boardRadius, + hexagonRadius: hexagonRadius, + hexagonMargin: hexagonMargin, + selected: boardPoint, + boardPoints: _boardPoints, + ); + return nextBoard; + } + + // Return a new board where boardPoint has the given color. + Board copyWithBoardPointColor(BoardPoint boardPoint, Color color) { + final BoardPoint nextBoardPoint = boardPoint.copyWithColor(color); + final int boardPointIndex = _boardPoints.indexWhere((BoardPoint boardPointI) => + boardPointI.q == boardPoint.q && boardPointI.r == boardPoint.r); + + if (elementAt(boardPointIndex) == boardPoint && boardPoint.color == color) { + return this; + } + + final List nextBoardPoints = List.from(_boardPoints); + nextBoardPoints[boardPointIndex] = nextBoardPoint; + final BoardPoint? selectedBoardPoint = + boardPoint == selected ? nextBoardPoint : selected; + return Board( + boardRadius: boardRadius, + hexagonRadius: hexagonRadius, + hexagonMargin: hexagonMargin, + selected: selectedBoardPoint, + boardPoints: nextBoardPoints, + ); + } +} + +class _BoardIterator implements Iterator { + _BoardIterator(this.boardPoints); + + final List boardPoints; + int? currentIndex; + + @override + BoardPoint? current; + + @override + bool moveNext() { + if (currentIndex == null) { + currentIndex = 0; + } else { + currentIndex = currentIndex! + 1; + } + + if (currentIndex! >= boardPoints.length) { + current = null; + return false; + } + + current = boardPoints[currentIndex!]; + return true; + } +} + +// A range of q/r board coordinate values. +@immutable +class _Range { + const _Range(this.min, this.max) : assert(min <= max); + + final int min; + final int max; +} + +// A location on the board in axial coordinates. +// Axial coordinates use two integers, q and r, to locate a hexagon on a grid. +// https://www.redblobgames.com/grids/hexagons/#coordinates-axial +@immutable +class BoardPoint { + const BoardPoint( + this.q, + this.r, { + this.color = const Color(0xFFCDCDCD), + }); + + final int q; + final int r; + final Color color; + + @override + String toString() { + return 'BoardPoint($q, $r, $color)'; + } + + // Only compares by location. + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is BoardPoint && other.q == q && other.r == r; + } + + @override + int get hashCode => Object.hash(q, r); + + BoardPoint copyWithColor(Color nextColor) => + BoardPoint(q, r, color: nextColor); + + // Convert from q,r axial coords to x,y,z cube coords. + Vector3 get cubeCoordinates { + return Vector3( + q.toDouble(), + r.toDouble(), + (-q - r).toDouble(), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/reference/transformations_demo_color_picker.dart b/dev/integration_tests/new_gallery/lib/demos/reference/transformations_demo_color_picker.dart new file mode 100644 index 0000000000..8f8fffc989 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/reference/transformations_demo_color_picker.dart @@ -0,0 +1,75 @@ +// 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:flutter/material.dart'; + +// A generic widget for a list of selectable colors. +@immutable +class ColorPicker extends StatelessWidget { + const ColorPicker({ + super.key, + required this.colors, + required this.selectedColor, + this.onColorSelection, + }); + + final Set colors; + final Color selectedColor; + final ValueChanged? onColorSelection; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: colors.map((Color color) { + return _ColorPickerSwatch( + color: color, + selected: color == selectedColor, + onTap: () { + if (onColorSelection != null) { + onColorSelection!(color); + } + }, + ); + }).toList(), + ); + } +} + +// A single selectable color widget in the ColorPicker. +@immutable +class _ColorPickerSwatch extends StatelessWidget { + const _ColorPickerSwatch({ + required this.color, + required this.selected, + this.onTap, + }); + + final Color color; + final bool selected; + final void Function()? onTap; + + @override + Widget build(BuildContext context) { + return Container( + width: 60, + height: 60, + padding: const EdgeInsets.fromLTRB(2, 0, 2, 0), + child: RawMaterialButton( + fillColor: color, + onPressed: () { + if (onTap != null) { + onTap!(); + } + }, + child: !selected + ? null + : const Icon( + Icons.check, + color: Colors.white, + ), + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/demos/reference/transformations_demo_edit_board_point.dart b/dev/integration_tests/new_gallery/lib/demos/reference/transformations_demo_edit_board_point.dart new file mode 100644 index 0000000000..c1742884c1 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/reference/transformations_demo_edit_board_point.dart @@ -0,0 +1,50 @@ +// 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:flutter/material.dart'; +import '../../themes/gallery_theme_data.dart'; +import 'transformations_demo_board.dart'; +import 'transformations_demo_color_picker.dart'; + +const Color backgroundColor = Color(0xFF272727); + +// The panel for editing a board point. +@immutable +class EditBoardPoint extends StatelessWidget { + const EditBoardPoint({ + super.key, + required this.boardPoint, + this.onColorSelection, + }); + + final BoardPoint boardPoint; + final ValueChanged? onColorSelection; + + @override + Widget build(BuildContext context) { + final Set boardPointColors = { + Colors.white, + GalleryThemeData.darkColorScheme.primary, + GalleryThemeData.darkColorScheme.primaryContainer, + GalleryThemeData.darkColorScheme.secondary, + backgroundColor, + }; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + '${boardPoint.q}, ${boardPoint.r}', + textAlign: TextAlign.right, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ColorPicker( + colors: boardPointColors, + selectedColor: boardPoint.color, + onColorSelection: onColorSelection, + ), + ], + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/demos/reference/two_pane_demo.dart b/dev/integration_tests/new_gallery/lib/demos/reference/two_pane_demo.dart new file mode 100644 index 0000000000..c8956a427f --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/reference/two_pane_demo.dart @@ -0,0 +1,229 @@ +// 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 'dart:ui'; +import 'package:dual_screen/dual_screen.dart'; +import 'package:flutter/material.dart'; +import '../../gallery_localizations.dart'; + +// BEGIN twoPaneDemo + +enum TwoPaneDemoType { + foldable, + tablet, + smallScreen, +} + +class TwoPaneDemo extends StatefulWidget { + const TwoPaneDemo({ + super.key, + required this.restorationId, + required this.type, + }); + + final String restorationId; + final TwoPaneDemoType type; + + @override + TwoPaneDemoState createState() => TwoPaneDemoState(); +} + +class TwoPaneDemoState extends State with RestorationMixin { + final RestorableInt _currentIndex = RestorableInt(-1); + + @override + String get restorationId => widget.restorationId; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_currentIndex, 'two_pane_selected_item'); + } + + @override + void dispose() { + _currentIndex.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + TwoPanePriority panePriority = TwoPanePriority.both; + if (widget.type == TwoPaneDemoType.smallScreen) { + panePriority = _currentIndex.value == -1 + ? TwoPanePriority.start + : TwoPanePriority.end; + } + return SimulateScreen( + type: widget.type, + child: TwoPane( + paneProportion: 0.3, + panePriority: panePriority, + startPane: ListPane( + selectedIndex: _currentIndex.value, + onSelect: (int index) { + setState(() { + _currentIndex.value = index; + }); + }, + ), + endPane: DetailsPane( + selectedIndex: _currentIndex.value, + onClose: widget.type == TwoPaneDemoType.smallScreen + ? () { + setState(() { + _currentIndex.value = -1; + }); + } + : null, + ), + ), + ); + } +} + +class ListPane extends StatelessWidget { + + const ListPane({ + super.key, + required this.onSelect, + required this.selectedIndex, + }); + final ValueChanged onSelect; + final int selectedIndex; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(GalleryLocalizations.of(context)!.demoTwoPaneList), + ), + body: Scrollbar( + child: ListView( + restorationId: 'list_demo_list_view', + padding: const EdgeInsets.symmetric(vertical: 8), + children: [ + for (int index = 1; index < 21; index++) + ListTile( + onTap: () { + onSelect(index); + }, + selected: selectedIndex == index, + leading: ExcludeSemantics( + child: CircleAvatar(child: Text('$index')), + ), + title: Text( + GalleryLocalizations.of(context)!.demoTwoPaneItem(index), + ), + ), + ], + ), + ), + ); + } +} + +class DetailsPane extends StatelessWidget { + + const DetailsPane({ + super.key, + required this.selectedIndex, + this.onClose, + }); + final VoidCallback? onClose; + final int selectedIndex; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + leading: onClose == null + ? null + : IconButton(icon: const Icon(Icons.close), onPressed: onClose), + title: Text( + GalleryLocalizations.of(context)!.demoTwoPaneDetails, + ), + ), + body: ColoredBox( + color: const Color(0xfffafafa), + child: Center( + child: Text( + selectedIndex == -1 + ? GalleryLocalizations.of(context)!.demoTwoPaneSelectItem + : GalleryLocalizations.of(context)! + .demoTwoPaneItemDetails(selectedIndex), + ), + ), + ), + ); + } +} + +class SimulateScreen extends StatelessWidget { + const SimulateScreen({ + super.key, + required this.type, + required this.child, + }); + + final TwoPaneDemoType type; + final TwoPane child; + + // An approximation of a real foldable + static const double foldableAspectRatio = 20 / 18; + // 16x9 candy bar phone + static const double singleScreenAspectRatio = 9 / 16; + // Taller desktop / tablet + static const double tabletAspectRatio = 4 / 3; + // How wide should the hinge be, as a proportion of total width + static const double hingeProportion = 1 / 35; + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(16), + ), + padding: const EdgeInsets.all(14), + child: AspectRatio( + aspectRatio: type == TwoPaneDemoType.foldable + ? foldableAspectRatio + : type == TwoPaneDemoType.tablet + ? tabletAspectRatio + : singleScreenAspectRatio, + child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { + final Size size = Size(constraints.maxWidth, constraints.maxHeight); + final Size hingeSize = Size(size.width * hingeProportion, size.height); + // Position the hinge in the middle of the display + final Rect hingeBounds = Rect.fromLTWH( + (size.width - hingeSize.width) / 2, + 0, + hingeSize.width, + hingeSize.height, + ); + return MediaQuery( + data: MediaQueryData( + size: size, + displayFeatures: [ + if (type == TwoPaneDemoType.foldable) + DisplayFeature( + bounds: hingeBounds, + type: DisplayFeatureType.hinge, + state: DisplayFeatureState.postureFlat, + ), + ], + ), + child: child, + ); + }), + ), + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/demos/reference/typography_demo.dart b/dev/integration_tests/new_gallery/lib/demos/reference/typography_demo.dart new file mode 100644 index 0000000000..1ef872f2cf --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/reference/typography_demo.dart @@ -0,0 +1,125 @@ +// 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:flutter/material.dart'; + +import '../../gallery_localizations.dart'; + +// BEGIN typographyDemo + +class _TextStyleItem extends StatelessWidget { + const _TextStyleItem({ + required this.name, + required this.style, + required this.text, + }); + + final String name; + final TextStyle style; + final String text; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), + child: Row( + children: [ + SizedBox( + width: 72, + child: Text(name, style: Theme.of(context).textTheme.bodySmall), + ), + Expanded( + child: Text(text, style: style), + ), + ], + ), + ); + } +} + +class TypographyDemo extends StatelessWidget { + const TypographyDemo({super.key}); + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + final List<_TextStyleItem> styleItems = <_TextStyleItem>[ + _TextStyleItem( + name: 'Headline 1', + style: textTheme.displayLarge!, + text: 'Light 96sp', + ), + _TextStyleItem( + name: 'Headline 2', + style: textTheme.displayMedium!, + text: 'Light 60sp', + ), + _TextStyleItem( + name: 'Headline 3', + style: textTheme.displaySmall!, + text: 'Regular 48sp', + ), + _TextStyleItem( + name: 'Headline 4', + style: textTheme.headlineMedium!, + text: 'Regular 34sp', + ), + _TextStyleItem( + name: 'Headline 5', + style: textTheme.headlineSmall!, + text: 'Regular 24sp', + ), + _TextStyleItem( + name: 'Headline 6', + style: textTheme.titleLarge!, + text: 'Medium 20sp', + ), + _TextStyleItem( + name: 'Subtitle 1', + style: textTheme.titleMedium!, + text: 'Regular 16sp', + ), + _TextStyleItem( + name: 'Subtitle 2', + style: textTheme.titleSmall!, + text: 'Medium 14sp', + ), + _TextStyleItem( + name: 'Body Text 1', + style: textTheme.bodyLarge!, + text: 'Regular 16sp', + ), + _TextStyleItem( + name: 'Body Text 2', + style: textTheme.bodyMedium!, + text: 'Regular 14sp', + ), + _TextStyleItem( + name: 'Button', + style: textTheme.labelLarge!, + text: 'MEDIUM (ALL CAPS) 14sp', + ), + _TextStyleItem( + name: 'Caption', + style: textTheme.bodySmall!, + text: 'Regular 12sp', + ), + _TextStyleItem( + name: 'Overline', + style: textTheme.labelSmall!, + text: 'REGULAR (ALL CAPS) 10sp', + ), + ]; + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(GalleryLocalizations.of(context)!.demoTypographyTitle), + ), + body: Scrollbar(child: ListView(children: styleItems)), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/feature_discovery/animation.dart b/dev/integration_tests/new_gallery/lib/feature_discovery/animation.dart new file mode 100644 index 0000000000..7b0c4e09f8 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/feature_discovery/animation.dart @@ -0,0 +1,251 @@ +// 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:flutter/material.dart'; + +/// Animations class to compute animation values for overlay widgets. +/// +/// Values are loosely based on Material Design specs, which are minimal. +class Animations { + + Animations( + this.openController, + this.tapController, + this.rippleController, + this.dismissController, + ); + final AnimationController openController; + final AnimationController tapController; + final AnimationController rippleController; + final AnimationController dismissController; + + static const double backgroundMaxOpacity = 0.96; + static const double backgroundTapRadius = 20.0; + static const double rippleMaxOpacity = 0.75; + static const double tapTargetToContentDistance = 20.0; + static const double tapTargetMaxRadius = 44.0; + static const double tapTargetMinRadius = 20.0; + static const double tapTargetRippleRadius = 64.0; + + Animation backgroundOpacity(FeatureDiscoveryStatus status) { + switch (status) { + case FeatureDiscoveryStatus.closed: + return const AlwaysStoppedAnimation(0); + case FeatureDiscoveryStatus.open: + return Tween(begin: 0, end: backgroundMaxOpacity) + .animate(CurvedAnimation( + parent: openController, + curve: const Interval(0, 0.5, curve: Curves.ease), + )); + case FeatureDiscoveryStatus.tap: + return Tween(begin: backgroundMaxOpacity, end: 0) + .animate(CurvedAnimation( + parent: tapController, + curve: Curves.ease, + )); + case FeatureDiscoveryStatus.dismiss: + return Tween(begin: backgroundMaxOpacity, end: 0) + .animate(CurvedAnimation( + parent: dismissController, + curve: const Interval(0.2, 1.0, curve: Curves.ease), + )); + case FeatureDiscoveryStatus.ripple: + return const AlwaysStoppedAnimation(backgroundMaxOpacity); + } + } + + Animation backgroundRadius( + FeatureDiscoveryStatus status, + double backgroundRadiusMax, + ) { + switch (status) { + case FeatureDiscoveryStatus.closed: + return const AlwaysStoppedAnimation(0); + case FeatureDiscoveryStatus.open: + return Tween(begin: 0, end: backgroundRadiusMax) + .animate(CurvedAnimation( + parent: openController, + curve: const Interval(0, 0.5, curve: Curves.ease), + )); + case FeatureDiscoveryStatus.tap: + return Tween( + begin: backgroundRadiusMax, + end: backgroundRadiusMax + backgroundTapRadius) + .animate(CurvedAnimation( + parent: tapController, + curve: Curves.ease, + )); + case FeatureDiscoveryStatus.dismiss: + return Tween(begin: backgroundRadiusMax, end: 0) + .animate(CurvedAnimation( + parent: dismissController, + curve: Curves.ease, + )); + case FeatureDiscoveryStatus.ripple: + return AlwaysStoppedAnimation(backgroundRadiusMax); + } + } + + Animation backgroundCenter( + FeatureDiscoveryStatus status, + Offset start, + Offset end, + ) { + switch (status) { + case FeatureDiscoveryStatus.closed: + return AlwaysStoppedAnimation(start); + case FeatureDiscoveryStatus.open: + return Tween(begin: start, end: end).animate(CurvedAnimation( + parent: openController, + curve: const Interval(0, 0.5, curve: Curves.ease), + )); + case FeatureDiscoveryStatus.tap: + return Tween(begin: end, end: start).animate(CurvedAnimation( + parent: tapController, + curve: Curves.ease, + )); + case FeatureDiscoveryStatus.dismiss: + return Tween(begin: end, end: start).animate(CurvedAnimation( + parent: dismissController, + curve: Curves.ease, + )); + case FeatureDiscoveryStatus.ripple: + return AlwaysStoppedAnimation(end); + } + } + + Animation contentOpacity(FeatureDiscoveryStatus status) { + switch (status) { + case FeatureDiscoveryStatus.closed: + return const AlwaysStoppedAnimation(0); + case FeatureDiscoveryStatus.open: + return Tween(begin: 0, end: 1.0).animate(CurvedAnimation( + parent: openController, + curve: const Interval(0.4, 0.7, curve: Curves.ease), + )); + case FeatureDiscoveryStatus.tap: + return Tween(begin: 1.0, end: 0).animate(CurvedAnimation( + parent: tapController, + curve: const Interval(0, 0.4, curve: Curves.ease), + )); + case FeatureDiscoveryStatus.dismiss: + return Tween(begin: 1.0, end: 0).animate(CurvedAnimation( + parent: dismissController, + curve: const Interval(0, 0.4, curve: Curves.ease), + )); + case FeatureDiscoveryStatus.ripple: + return const AlwaysStoppedAnimation(1.0); + } + } + + Animation rippleOpacity(FeatureDiscoveryStatus status) { + switch (status) { + case FeatureDiscoveryStatus.ripple: + return Tween(begin: rippleMaxOpacity, end: 0) + .animate(CurvedAnimation( + parent: rippleController, + curve: const Interval(0.3, 0.8, curve: Curves.ease), + )); + case FeatureDiscoveryStatus.closed: + case FeatureDiscoveryStatus.open: + case FeatureDiscoveryStatus.tap: + case FeatureDiscoveryStatus.dismiss: + return const AlwaysStoppedAnimation(0); + } + } + + Animation rippleRadius(FeatureDiscoveryStatus status) { + switch (status) { + case FeatureDiscoveryStatus.ripple: + if (rippleController.value >= 0.3 && rippleController.value <= 0.8) { + return Tween(begin: tapTargetMaxRadius, end: 79.0) + .animate(CurvedAnimation( + parent: rippleController, + curve: const Interval(0.3, 0.8, curve: Curves.ease), + )); + } + return const AlwaysStoppedAnimation(tapTargetMaxRadius); + case FeatureDiscoveryStatus.closed: + case FeatureDiscoveryStatus.open: + case FeatureDiscoveryStatus.tap: + case FeatureDiscoveryStatus.dismiss: + return const AlwaysStoppedAnimation(0); + } + } + + Animation tapTargetOpacity(FeatureDiscoveryStatus status) { + switch (status) { + case FeatureDiscoveryStatus.closed: + return const AlwaysStoppedAnimation(0); + case FeatureDiscoveryStatus.open: + return Tween(begin: 0, end: 1.0).animate(CurvedAnimation( + parent: openController, + curve: const Interval(0, 0.4, curve: Curves.ease), + )); + case FeatureDiscoveryStatus.tap: + return Tween(begin: 1.0, end: 0).animate(CurvedAnimation( + parent: tapController, + curve: const Interval(0.1, 0.6, curve: Curves.ease), + )); + case FeatureDiscoveryStatus.dismiss: + return Tween(begin: 1.0, end: 0).animate(CurvedAnimation( + parent: dismissController, + curve: const Interval(0.2, 0.8, curve: Curves.ease), + )); + case FeatureDiscoveryStatus.ripple: + return const AlwaysStoppedAnimation(1.0); + } + } + + Animation tapTargetRadius(FeatureDiscoveryStatus status) { + switch (status) { + case FeatureDiscoveryStatus.closed: + return const AlwaysStoppedAnimation(tapTargetMinRadius); + case FeatureDiscoveryStatus.open: + return Tween(begin: tapTargetMinRadius, end: tapTargetMaxRadius) + .animate(CurvedAnimation( + parent: openController, + curve: const Interval(0, 0.4, curve: Curves.ease), + )); + case FeatureDiscoveryStatus.ripple: + if (rippleController.value < 0.3) { + return Tween( + begin: tapTargetMaxRadius, end: tapTargetRippleRadius) + .animate(CurvedAnimation( + parent: rippleController, + curve: const Interval(0, 0.3, curve: Curves.ease), + )); + } else if (rippleController.value < 0.6) { + return Tween( + begin: tapTargetRippleRadius, end: tapTargetMaxRadius) + .animate(CurvedAnimation( + parent: rippleController, + curve: const Interval(0.3, 0.6, curve: Curves.ease), + )); + } + return const AlwaysStoppedAnimation(tapTargetMaxRadius); + case FeatureDiscoveryStatus.tap: + return Tween(begin: tapTargetMaxRadius, end: tapTargetMinRadius) + .animate(CurvedAnimation( + parent: tapController, + curve: Curves.ease, + )); + case FeatureDiscoveryStatus.dismiss: + return Tween(begin: tapTargetMaxRadius, end: tapTargetMinRadius) + .animate(CurvedAnimation( + parent: dismissController, + curve: Curves.ease, + )); + } + } +} + +/// Enum to indicate the current status of a [FeatureDiscovery] widget. +enum FeatureDiscoveryStatus { + closed, // Overlay is closed. + open, // Overlay is opening. + ripple, // Overlay is rippling. + tap, // Overlay is tapped. + dismiss, // Overlay is being dismissed. +} diff --git a/dev/integration_tests/new_gallery/lib/feature_discovery/feature_discovery.dart b/dev/integration_tests/new_gallery/lib/feature_discovery/feature_discovery.dart new file mode 100644 index 0000000000..115657227d --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/feature_discovery/feature_discovery.dart @@ -0,0 +1,384 @@ +// 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:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +import 'animation.dart'; +import 'overlay.dart'; + +/// [Widget] to enforce a global lock system for [FeatureDiscovery] widgets. +/// +/// This widget enforces that at most one [FeatureDiscovery] widget in its +/// widget tree is shown at a time. +/// +/// Users wanting to use [FeatureDiscovery] need to put this controller +/// above [FeatureDiscovery] widgets in the widget tree. +class FeatureDiscoveryController extends StatefulWidget { + + const FeatureDiscoveryController(this.child, {super.key}); + final Widget child; + + static _FeatureDiscoveryControllerState _of(BuildContext context) { + final _FeatureDiscoveryControllerState? matchResult = + context.findAncestorStateOfType<_FeatureDiscoveryControllerState>(); + if (matchResult != null) { + return matchResult; + } + + throw FlutterError( + 'FeatureDiscoveryController.of() called with a context that does not ' + 'contain a FeatureDiscoveryController.\n The context used was:\n ' + '$context'); + } + + @override + State createState() => + _FeatureDiscoveryControllerState(); +} + +class _FeatureDiscoveryControllerState + extends State { + bool _isLocked = false; + + /// Flag to indicate whether a [FeatureDiscovery] widget descendant is + /// currently showing its overlay or not. + /// + /// If true, then no other [FeatureDiscovery] widget should display its + /// overlay. + bool get isLocked => _isLocked; + + /// Lock the controller. + /// + /// Note we do not [setState] here because this function will be called + /// by the first [FeatureDiscovery] ready to show its overlay, and any + /// additional [FeatureDiscovery] widgets wanting to show their overlays + /// will already be scheduled to be built, so the lock change will be caught + /// in their builds. + void lock() => _isLocked = true; + + /// Unlock the controller. + void unlock() => setState(() => _isLocked = false); + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + assert( + context.findAncestorStateOfType<_FeatureDiscoveryControllerState>() == + null, + 'There should not be another ancestor of type ' + 'FeatureDiscoveryController in the widget tree.', + ); + } + + @override + Widget build(BuildContext context) => widget.child; +} + +/// Widget that highlights the [child] with an overlay. +/// +/// This widget loosely follows the guidelines set forth in the Material Specs: +/// https://material.io/archive/guidelines/growth-communications/feature-discovery.html. +class FeatureDiscovery extends StatefulWidget { + + const FeatureDiscovery({ + super.key, + required this.title, + required this.description, + required this.child, + required this.showOverlay, + this.onDismiss, + this.onTap, + this.color, + }); + /// Title to be displayed in the overlay. + final String title; + + /// Description to be displayed in the overlay. + final String description; + + /// Icon to be promoted. + final Icon child; + + /// Flag to indicate whether to show the overlay or not anchored to the + /// [child]. + final bool showOverlay; + + /// Callback invoked when the user dismisses an overlay. + final void Function()? onDismiss; + + /// Callback invoked when the user taps on the tap target of an overlay. + final void Function()? onTap; + + /// Color with which to fill the outer circle. + final Color? color; + + @visibleForTesting + static const Key overlayKey = Key('overlay key'); + + @visibleForTesting + static const Key gestureDetectorKey = Key('gesture detector key'); + + @override + State createState() => _FeatureDiscoveryState(); +} + +class _FeatureDiscoveryState extends State + with TickerProviderStateMixin { + bool showOverlay = false; + FeatureDiscoveryStatus status = FeatureDiscoveryStatus.closed; + + late AnimationController openController; + late AnimationController rippleController; + late AnimationController tapController; + late AnimationController dismissController; + + late Animations animations; + OverlayEntry? overlay; + + Widget buildOverlay(BuildContext ctx, Offset center) { + debugCheckHasMediaQuery(ctx); + debugCheckHasDirectionality(ctx); + + final Size deviceSize = MediaQuery.of(ctx).size; + final Color color = widget.color ?? Theme.of(ctx).colorScheme.primary; + + // Wrap in transparent [Material] to enable widgets that require one. + return Material( + key: FeatureDiscovery.overlayKey, + type: MaterialType.transparency, + child: Stack( + children: [ + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + key: FeatureDiscovery.gestureDetectorKey, + onTap: dismiss, + child: Container( + width: double.infinity, + height: double.infinity, + color: Colors.transparent, + ), + ), + ), + Background( + animations: animations, + status: status, + color: color, + center: center, + deviceSize: deviceSize, + textDirection: Directionality.of(ctx), + ), + Content( + animations: animations, + status: status, + center: center, + deviceSize: deviceSize, + title: widget.title, + description: widget.description, + textTheme: Theme.of(ctx).textTheme, + ), + Ripple( + animations: animations, + status: status, + center: center, + ), + TapTarget( + animations: animations, + status: status, + center: center, + onTap: tap, + child: widget.child, + ), + ], + ), + ); + } + + /// Method to handle user tap on [TapTarget]. + /// + /// Tapping will stop any active controller and start the [tapController]. + void tap() { + openController.stop(); + rippleController.stop(); + dismissController.stop(); + tapController.forward(from: 0.0); + } + + /// Method to handle user dismissal. + /// + /// Dismissal will stop any active controller and start the + /// [dismissController]. + void dismiss() { + openController.stop(); + rippleController.stop(); + tapController.stop(); + dismissController.forward(from: 0.0); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (BuildContext ctx, _) { + if (overlay != null) { + SchedulerBinding.instance.addPostFrameCallback((_) { + // [OverlayEntry] needs to be explicitly rebuilt when necessary. + overlay!.markNeedsBuild(); + }); + } else { + if (showOverlay && !FeatureDiscoveryController._of(ctx).isLocked) { + final OverlayEntry entry = OverlayEntry( + builder: (_) => buildOverlay(ctx, getOverlayCenter(ctx)), + ); + + // Lock [FeatureDiscoveryController] early in order to prevent + // another [FeatureDiscovery] widget from trying to show its + // overlay while the post frame callback and set state are not + // complete. + FeatureDiscoveryController._of(ctx).lock(); + + SchedulerBinding.instance.addPostFrameCallback((_) { + setState(() { + overlay = entry; + status = FeatureDiscoveryStatus.closed; + openController.forward(from: 0.0); + }); + Overlay.of(context).insert(entry); + }); + } + } + return widget.child; + }); + } + + /// Compute the center position of the overlay. + Offset getOverlayCenter(BuildContext parentCtx) { + final RenderBox box = parentCtx.findRenderObject()! as RenderBox; + final Size size = box.size; + final Offset topLeftPosition = box.localToGlobal(Offset.zero); + final Offset centerPosition = Offset( + topLeftPosition.dx + size.width / 2, + topLeftPosition.dy + size.height / 2, + ); + return centerPosition; + } + + static bool _featureHighlightShown = false; + + @override + void initState() { + super.initState(); + + initAnimationControllers(); + initAnimations(); + + showOverlay = widget.showOverlay && !_featureHighlightShown; + if (showOverlay) { + _featureHighlightShown = true; + } + } + + void initAnimationControllers() { + openController = AnimationController( + duration: const Duration(milliseconds: 500), + vsync: this, + ) + ..addListener(() { + setState(() {}); + }) + ..addStatusListener((AnimationStatus animationStatus) { + if (animationStatus == AnimationStatus.forward) { + setState(() => status = FeatureDiscoveryStatus.open); + } else if (animationStatus == AnimationStatus.completed) { + rippleController.forward(from: 0.0); + } + }); + + rippleController = AnimationController( + duration: const Duration(milliseconds: 1000), + vsync: this, + ) + ..addListener(() { + setState(() {}); + }) + ..addStatusListener((AnimationStatus animationStatus) { + if (animationStatus == AnimationStatus.forward) { + setState(() => status = FeatureDiscoveryStatus.ripple); + } else if (animationStatus == AnimationStatus.completed) { + rippleController.forward(from: 0.0); + } + }); + + tapController = AnimationController( + duration: const Duration(milliseconds: 250), + vsync: this, + ) + ..addListener(() { + setState(() {}); + }) + ..addStatusListener((AnimationStatus animationStatus) { + if (animationStatus == AnimationStatus.forward) { + setState(() => status = FeatureDiscoveryStatus.tap); + } else if (animationStatus == AnimationStatus.completed) { + widget.onTap?.call(); + cleanUponOverlayClose(); + } + }); + + dismissController = AnimationController( + duration: const Duration(milliseconds: 250), + vsync: this, + ) + ..addListener(() { + setState(() {}); + }) + ..addStatusListener((AnimationStatus animationStatus) { + if (animationStatus == AnimationStatus.forward) { + setState(() => status = FeatureDiscoveryStatus.dismiss); + } else if (animationStatus == AnimationStatus.completed) { + widget.onDismiss?.call(); + cleanUponOverlayClose(); + } + }); + } + + void initAnimations() { + animations = Animations( + openController, + tapController, + rippleController, + dismissController, + ); + } + + /// Clean up once overlay has been dismissed or tap target has been tapped. + /// + /// This is called upon [tapController] and [dismissController] end. + void cleanUponOverlayClose() { + FeatureDiscoveryController._of(context).unlock(); + setState(() { + status = FeatureDiscoveryStatus.closed; + showOverlay = false; + overlay?.remove(); + overlay = null; + }); + } + + @override + void didUpdateWidget(FeatureDiscovery oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.showOverlay != oldWidget.showOverlay) { + showOverlay = widget.showOverlay; + } + } + + @override + void dispose() { + overlay?.remove(); + openController.dispose(); + rippleController.dispose(); + tapController.dispose(); + dismissController.dispose(); + super.dispose(); + } +} diff --git a/dev/integration_tests/new_gallery/lib/feature_discovery/overlay.dart b/dev/integration_tests/new_gallery/lib/feature_discovery/overlay.dart new file mode 100644 index 0000000000..9052c0cdc4 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/feature_discovery/overlay.dart @@ -0,0 +1,382 @@ +// 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 'dart:math'; + +import 'package:flutter/material.dart'; +import 'animation.dart'; + +const double contentHeight = 80.0; +const double contentWidth = 300.0; +const double contentHorizontalPadding = 40.0; +const double tapTargetRadius = 44.0; +const double tapTargetToContentDistance = 20.0; +const double gutterHeight = 88.0; + +/// Background of the overlay. +class Background extends StatelessWidget { + + const Background({ + super.key, + required this.animations, + required this.center, + required this.color, + required this.deviceSize, + required this.status, + required this.textDirection, + }); + /// Animations. + final Animations animations; + + /// Overlay center position. + final Offset center; + + /// Color of the background. + final Color color; + + /// Device size. + final Size deviceSize; + + /// Status of the parent overlay. + final FeatureDiscoveryStatus status; + + /// Directionality of content. + final TextDirection textDirection; + + static const double horizontalShift = 20.0; + static const double padding = 40.0; + + /// Compute the center position of the background. + /// + /// If [center] is near the top or bottom edges of the screen, then + /// background is centered there. + /// Otherwise, background center is calculated and upon opening, animated + /// from [center] to the new calculated position. + Offset get centerPosition { + if (_isNearTopOrBottomEdges(center, deviceSize)) { + return center; + } else { + final Offset start = center; + + // dy of centerPosition is calculated to be the furthest point in + // [Content] from the [center]. + double endY; + if (_isOnTopHalfOfScreen(center, deviceSize)) { + endY = center.dy - + tapTargetRadius - + tapTargetToContentDistance - + contentHeight; + if (endY < 0.0) { + endY = center.dy + tapTargetRadius + tapTargetToContentDistance; + } + } else { + endY = center.dy + tapTargetRadius + tapTargetToContentDistance; + if (endY + contentHeight > deviceSize.height) { + endY = center.dy - + tapTargetRadius - + tapTargetToContentDistance - + contentHeight; + } + } + + // Horizontal background center shift based on whether the tap target is + // on the left, center, or right side of the screen. + double shift; + if (_isOnLeftHalfOfScreen(center, deviceSize)) { + shift = horizontalShift; + } else if (center.dx == deviceSize.width / 2) { + shift = textDirection == TextDirection.ltr + ? -horizontalShift + : horizontalShift; + } else { + shift = -horizontalShift; + } + + // dx of centerPosition is calculated to be the middle point of the + // [Content] bounds shifted by [horizontalShift]. + final Rect textBounds = _getContentBounds(deviceSize, center); + final double left = min(textBounds.left, center.dx - 88.0); + final double right = max(textBounds.right, center.dx + 88.0); + final double endX = (left + right) / 2 + shift; + final Offset end = Offset(endX, endY); + + return animations.backgroundCenter(status, start, end).value; + } + } + + /// Compute the radius. + /// + /// Radius is a function of the greatest distance from [center] to one of + /// the corners of [Content]. + double get radius { + final Rect textBounds = _getContentBounds(deviceSize, center); + final double textRadius = _maxDistance(center, textBounds) + padding; + if (_isNearTopOrBottomEdges(center, deviceSize)) { + return animations.backgroundRadius(status, textRadius).value; + } else { + // Scale down radius if icon is towards the middle of the screen. + return animations.backgroundRadius(status, textRadius).value * 0.8; + } + } + + double get opacity => animations.backgroundOpacity(status).value; + + @override + Widget build(BuildContext context) { + return Positioned( + left: centerPosition.dx, + top: centerPosition.dy, + child: FractionalTranslation( + translation: const Offset(-0.5, -0.5), + child: Opacity( + opacity: opacity, + child: Container( + height: radius * 2, + width: radius * 2, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color, + ), + ), + ), + )); + } + + /// Compute the maximum distance from [point] to the four corners of [bounds]. + double _maxDistance(Offset point, Rect bounds) { + double distance(double x1, double y1, double x2, double y2) { + return sqrt(pow(x2 - x1, 2) + pow(y2 - y1, 2)); + } + + final double tl = distance(point.dx, point.dy, bounds.left, bounds.top); + final double tr = distance(point.dx, point.dy, bounds.right, bounds.top); + final double bl = distance(point.dx, point.dy, bounds.left, bounds.bottom); + final double br = distance(point.dx, point.dy, bounds.right, bounds.bottom); + return max(tl, max(tr, max(bl, br))); + } +} + +/// Widget that represents the text to show in the overlay. +class Content extends StatelessWidget { + + const Content({ + super.key, + required this.animations, + required this.center, + required this.description, + required this.deviceSize, + required this.status, + required this.title, + required this.textTheme, + }); + /// Animations. + final Animations animations; + + /// Overlay center position. + final Offset center; + + /// Description. + final String description; + + /// Device size. + final Size deviceSize; + + /// Status of the parent overlay. + final FeatureDiscoveryStatus status; + + /// Title. + final String title; + + /// [TextTheme] to use for drawing the [title] and the [description]. + final TextTheme textTheme; + + double get opacity => animations.contentOpacity(status).value; + + @override + Widget build(BuildContext context) { + final Rect position = _getContentBounds(deviceSize, center); + + return Positioned( + left: position.left, + height: position.bottom - position.top, + width: position.right - position.left, + top: position.top, + child: Opacity( + opacity: opacity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTitle(textTheme), + const SizedBox(height: 12.0), + _buildDescription(textTheme), + ], + ), + ), + ); + } + + Widget _buildTitle(TextTheme theme) { + return Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.titleLarge?.copyWith(color: Colors.white), + ); + } + + Widget _buildDescription(TextTheme theme) { + return Text( + description, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: theme.titleMedium?.copyWith(color: Colors.white70), + ); + } +} + +/// Widget that represents the ripple effect of [TapTarget]. +class Ripple extends StatelessWidget { + + const Ripple({ + super.key, + required this.animations, + required this.center, + required this.status, + }); + /// Animations. + final Animations animations; + + /// Overlay center position. + final Offset center; + + /// Status of the parent overlay. + final FeatureDiscoveryStatus status; + + double get radius => animations.rippleRadius(status).value; + double get opacity => animations.rippleOpacity(status).value; + + @override + Widget build(BuildContext context) { + return Positioned( + left: center.dx, + top: center.dy, + child: FractionalTranslation( + translation: const Offset(-0.5, -0.5), + child: Opacity( + opacity: opacity, + child: Container( + height: radius * 2, + width: radius * 2, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + ), + ), + ); + } +} + +/// Wrapper widget around [child] representing the anchor of the overlay. +class TapTarget extends StatelessWidget { + + const TapTarget({ + super.key, + required this.animations, + required this.center, + required this.status, + required this.onTap, + required this.child, + }); + /// Animations. + final Animations animations; + + /// Device size. + final Offset center; + + /// Status of the parent overlay. + final FeatureDiscoveryStatus status; + + /// Callback invoked when the user taps on the [TapTarget]. + final void Function() onTap; + + /// Child widget that will be promoted by the overlay. + final Icon child; + + double get radius => animations.tapTargetRadius(status).value; + double get opacity => animations.tapTargetOpacity(status).value; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + return Positioned( + left: center.dx, + top: center.dy, + child: FractionalTranslation( + translation: const Offset(-0.5, -0.5), + child: InkWell( + onTap: onTap, + child: Opacity( + opacity: opacity, + child: Container( + height: radius * 2, + width: radius * 2, + decoration: BoxDecoration( + color: theme.brightness == Brightness.dark + ? theme.colorScheme.primary + : Colors.white, + shape: BoxShape.circle, + ), + child: child, + ), + ), + ), + ), + ); + } +} + +/// Method to compute the bounds of the content. +/// +/// This is exposed so it can be used for calculating the background radius +/// and center and for laying out the content. +Rect _getContentBounds(Size deviceSize, Offset overlayCenter) { + double top; + if (_isOnTopHalfOfScreen(overlayCenter, deviceSize)) { + top = overlayCenter.dy - + tapTargetRadius - + tapTargetToContentDistance - + contentHeight; + if (top < 0) { + top = overlayCenter.dy + tapTargetRadius + tapTargetToContentDistance; + } + } else { + top = overlayCenter.dy + tapTargetRadius + tapTargetToContentDistance; + if (top + contentHeight > deviceSize.height) { + top = overlayCenter.dy - + tapTargetRadius - + tapTargetToContentDistance - + contentHeight; + } + } + + final double left = max(contentHorizontalPadding, overlayCenter.dx - contentWidth); + final double right = + min(deviceSize.width - contentHorizontalPadding, left + contentWidth); + return Rect.fromLTRB(left, top, right, top + contentHeight); +} + +bool _isNearTopOrBottomEdges(Offset position, Size deviceSize) { + return position.dy <= gutterHeight || + (deviceSize.height - position.dy) <= gutterHeight; +} + +bool _isOnTopHalfOfScreen(Offset position, Size deviceSize) { + return position.dy < (deviceSize.height / 2.0); +} + +bool _isOnLeftHalfOfScreen(Offset position, Size deviceSize) { + return position.dx < (deviceSize.width / 2.0); +} diff --git a/dev/integration_tests/new_gallery/lib/gallery_localizations.dart b/dev/integration_tests/new_gallery/lib/gallery_localizations.dart new file mode 100644 index 0000000000..2dacb92304 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/gallery_localizations.dart @@ -0,0 +1,4950 @@ +// 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 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'gallery_localizations_en.dart'; + +/// Callers can lookup localized strings with an instance of GalleryLocalizations +/// returned by `GalleryLocalizations.of(context)`. +/// +/// Applications need to include `GalleryLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'gen_l10n/gallery_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: GalleryLocalizations.localizationsDelegates, +/// supportedLocales: GalleryLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the GalleryLocalizations.supportedLocales +/// property. +abstract class GalleryLocalizations { + GalleryLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale); + + final String localeName; + + static GalleryLocalizations? of(BuildContext context) { + return Localizations.of(context, GalleryLocalizations); + } + + static const LocalizationsDelegate delegate = _GalleryLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('en'), + Locale('en', 'IS') + ]; + + /// Represents a link to a GitHub repository. + /// + /// In en, this message translates to: + /// **'{repoName} GitHub repository'** + String githubRepo(Object repoName); + + /// A description about how to view the source code for this app. + /// + /// In en, this message translates to: + /// **'To see the source code for this app, please visit the {repoLink}.'** + String aboutDialogDescription(Object repoLink); + + /// Deselect a (selectable) item + /// + /// In en, this message translates to: + /// **'Deselect'** + String get deselect; + + /// Indicates the status of a (selectable) item not being selected + /// + /// In en, this message translates to: + /// **'Not selected'** + String get notSelected; + + /// Select a (selectable) item + /// + /// In en, this message translates to: + /// **'Select'** + String get select; + + /// Indicates the associated piece of UI is selectable by long pressing it + /// + /// In en, this message translates to: + /// **'Selectable (long press)'** + String get selectable; + + /// Indicates status of a (selectable) item being selected + /// + /// In en, this message translates to: + /// **'Selected'** + String get selected; + + /// Sign in label to sign into website. + /// + /// In en, this message translates to: + /// **'SIGN IN'** + String get signIn; + + /// Password was updated on a different device and the user is required to sign in again + /// + /// In en, this message translates to: + /// **'Your password was updated on your other device. Please sign in again.'** + String get bannerDemoText; + + /// Show the Banner to the user again. + /// + /// In en, this message translates to: + /// **'Reset the banner'** + String get bannerDemoResetText; + + /// When the user clicks this button the Banner will toggle multiple actions or a single action + /// + /// In en, this message translates to: + /// **'Multiple actions'** + String get bannerDemoMultipleText; + + /// If user clicks this button the leading icon in the Banner will disappear + /// + /// In en, this message translates to: + /// **'Leading Icon'** + String get bannerDemoLeadingText; + + /// When text is pressed the banner widget will be removed from the screen. + /// + /// In en, this message translates to: + /// **'DISMISS'** + String get dismiss; + + /// Semantic label for back button to exit a study and return to the gallery home page. + /// + /// In en, this message translates to: + /// **'Back to Gallery'** + String get backToGallery; + + /// Click to see more about the content in the cards demo. + /// + /// In en, this message translates to: + /// **'Explore'** + String get cardsDemoExplore; + + /// Semantics label for Explore. Label tells user to explore the destinationName to the user. Example Explore Tamil + /// + /// In en, this message translates to: + /// **'Explore {destinationName}'** + String cardsDemoExploreSemantics(Object destinationName); + + /// Semantics label for Share. Label tells user to share the destinationName to the user. Example Share Tamil + /// + /// In en, this message translates to: + /// **'Share {destinationName}'** + String cardsDemoShareSemantics(Object destinationName); + + /// The user can tap this button + /// + /// In en, this message translates to: + /// **'Tappable'** + String get cardsDemoTappable; + + /// The top 10 cities that you can visit in Tamil Nadu + /// + /// In en, this message translates to: + /// **'Top 10 Cities to Visit in Tamil Nadu'** + String get cardsDemoTravelDestinationTitle1; + + /// Number 10 + /// + /// In en, this message translates to: + /// **'Number 10'** + String get cardsDemoTravelDestinationDescription1; + + /// Thanjavur the city + /// + /// In en, this message translates to: + /// **'Thanjavur'** + String get cardsDemoTravelDestinationCity1; + + /// Thanjavur, Tamil Nadu is a location + /// + /// In en, this message translates to: + /// **'Thanjavur, Tamil Nadu'** + String get cardsDemoTravelDestinationLocation1; + + /// Artist that are from Southern India + /// + /// In en, this message translates to: + /// **'Artisans of Southern India'** + String get cardsDemoTravelDestinationTitle2; + + /// Silk Spinners + /// + /// In en, this message translates to: + /// **'Silk Spinners'** + String get cardsDemoTravelDestinationDescription2; + + /// Chettinad the city + /// + /// In en, this message translates to: + /// **'Chettinad'** + String get cardsDemoTravelDestinationCity2; + + /// Sivaganga, Tamil Nadu is a location + /// + /// In en, this message translates to: + /// **'Sivaganga, Tamil Nadu'** + String get cardsDemoTravelDestinationLocation2; + + /// Brihadisvara Temple + /// + /// In en, this message translates to: + /// **'Brihadisvara Temple'** + String get cardsDemoTravelDestinationTitle3; + + /// Temples + /// + /// In en, this message translates to: + /// **'Temples'** + String get cardsDemoTravelDestinationDescription3; + + /// Header title on home screen for Gallery section. + /// + /// In en, this message translates to: + /// **'Gallery'** + String get homeHeaderGallery; + + /// Header title on home screen for Categories section. + /// + /// In en, this message translates to: + /// **'Categories'** + String get homeHeaderCategories; + + /// Study description for Shrine. + /// + /// In en, this message translates to: + /// **'A fashionable retail app'** + String get shrineDescription; + + /// Study description for Fortnightly. + /// + /// In en, this message translates to: + /// **'A content-focused news app'** + String get fortnightlyDescription; + + /// Study description for Rally. + /// + /// In en, this message translates to: + /// **'A personal finance app'** + String get rallyDescription; + + /// Study description for Reply. + /// + /// In en, this message translates to: + /// **'An efficient, focused email app'** + String get replyDescription; + + /// Name for account made up by user. + /// + /// In en, this message translates to: + /// **'Checking'** + String get rallyAccountDataChecking; + + /// Name for account made up by user. + /// + /// In en, this message translates to: + /// **'Home Savings'** + String get rallyAccountDataHomeSavings; + + /// Name for account made up by user. + /// + /// In en, this message translates to: + /// **'Car Savings'** + String get rallyAccountDataCarSavings; + + /// Name for account made up by user. + /// + /// In en, this message translates to: + /// **'Vacation'** + String get rallyAccountDataVacation; + + /// Title for account statistics. Below a percentage such as 0.10% will be displayed. + /// + /// In en, this message translates to: + /// **'Annual Percentage Yield'** + String get rallyAccountDetailDataAnnualPercentageYield; + + /// Title for account statistics. Below a dollar amount such as $100 will be displayed. + /// + /// In en, this message translates to: + /// **'Interest Rate'** + String get rallyAccountDetailDataInterestRate; + + /// Title for account statistics. Below a dollar amount such as $100 will be displayed. + /// + /// In en, this message translates to: + /// **'Interest YTD'** + String get rallyAccountDetailDataInterestYtd; + + /// Title for account statistics. Below a dollar amount such as $100 will be displayed. + /// + /// In en, this message translates to: + /// **'Interest Paid Last Year'** + String get rallyAccountDetailDataInterestPaidLastYear; + + /// Title for an account detail. Below a date for when the next account statement is released. + /// + /// In en, this message translates to: + /// **'Next Statement'** + String get rallyAccountDetailDataNextStatement; + + /// Title for an account detail. Below the name of the account owner will be displayed. + /// + /// In en, this message translates to: + /// **'Account Owner'** + String get rallyAccountDetailDataAccountOwner; + + /// Title for column where it displays the total dollar amount that the user has in bills. + /// + /// In en, this message translates to: + /// **'Total Amount'** + String get rallyBillDetailTotalAmount; + + /// Title for column where it displays the amount that the user has paid. + /// + /// In en, this message translates to: + /// **'Amount Paid'** + String get rallyBillDetailAmountPaid; + + /// Title for column where it displays the amount that the user has due. + /// + /// In en, this message translates to: + /// **'Amount Due'** + String get rallyBillDetailAmountDue; + + /// Category for budget, to sort expenses / bills in. + /// + /// In en, this message translates to: + /// **'Coffee Shops'** + String get rallyBudgetCategoryCoffeeShops; + + /// Category for budget, to sort expenses / bills in. + /// + /// In en, this message translates to: + /// **'Groceries'** + String get rallyBudgetCategoryGroceries; + + /// Category for budget, to sort expenses / bills in. + /// + /// In en, this message translates to: + /// **'Restaurants'** + String get rallyBudgetCategoryRestaurants; + + /// Category for budget, to sort expenses / bills in. + /// + /// In en, this message translates to: + /// **'Clothing'** + String get rallyBudgetCategoryClothing; + + /// Title for column where it displays the total dollar cap that the user has for its budget. + /// + /// In en, this message translates to: + /// **'Total Cap'** + String get rallyBudgetDetailTotalCap; + + /// Title for column where it displays the dollar amount that the user has used in its budget. + /// + /// In en, this message translates to: + /// **'Amount Used'** + String get rallyBudgetDetailAmountUsed; + + /// Title for column where it displays the dollar amount that the user has left in its budget. + /// + /// In en, this message translates to: + /// **'Amount Left'** + String get rallyBudgetDetailAmountLeft; + + /// Link to go to the page 'Manage Accounts. + /// + /// In en, this message translates to: + /// **'Manage Accounts'** + String get rallySettingsManageAccounts; + + /// Link to go to the page 'Tax Documents'. + /// + /// In en, this message translates to: + /// **'Tax Documents'** + String get rallySettingsTaxDocuments; + + /// Link to go to the page 'Passcode and Touch ID'. + /// + /// In en, this message translates to: + /// **'Passcode and Touch ID'** + String get rallySettingsPasscodeAndTouchId; + + /// Link to go to the page 'Notifications'. + /// + /// In en, this message translates to: + /// **'Notifications'** + String get rallySettingsNotifications; + + /// Link to go to the page 'Personal Information'. + /// + /// In en, this message translates to: + /// **'Personal Information'** + String get rallySettingsPersonalInformation; + + /// Link to go to the page 'Paperless Settings'. + /// + /// In en, this message translates to: + /// **'Paperless Settings'** + String get rallySettingsPaperlessSettings; + + /// Link to go to the page 'Find ATMs'. + /// + /// In en, this message translates to: + /// **'Find ATMs'** + String get rallySettingsFindAtms; + + /// Link to go to the page 'Help'. + /// + /// In en, this message translates to: + /// **'Help'** + String get rallySettingsHelp; + + /// Link to go to the page 'Sign out'. + /// + /// In en, this message translates to: + /// **'Sign out'** + String get rallySettingsSignOut; + + /// Title for 'total account value' overview page, a dollar value is displayed next to it. + /// + /// In en, this message translates to: + /// **'Total'** + String get rallyAccountTotal; + + /// Title for 'bills due' page, a dollar value is displayed next to it. + /// + /// In en, this message translates to: + /// **'Due'** + String get rallyBillsDue; + + /// Title for 'budget left' page, a dollar value is displayed next to it. + /// + /// In en, this message translates to: + /// **'Left'** + String get rallyBudgetLeft; + + /// Link text for accounts page. + /// + /// In en, this message translates to: + /// **'Accounts'** + String get rallyAccounts; + + /// Link text for bills page. + /// + /// In en, this message translates to: + /// **'Bills'** + String get rallyBills; + + /// Link text for budgets page. + /// + /// In en, this message translates to: + /// **'Budgets'** + String get rallyBudgets; + + /// Title for alerts part of overview page. + /// + /// In en, this message translates to: + /// **'Alerts'** + String get rallyAlerts; + + /// Link text for button to see all data for category. + /// + /// In en, this message translates to: + /// **'SEE ALL'** + String get rallySeeAll; + + /// Displayed as 'dollar amount left', for example $46.70 LEFT, for a budget category. + /// + /// In en, this message translates to: + /// **' LEFT'** + String get rallyFinanceLeft; + + /// The navigation link to the overview page. + /// + /// In en, this message translates to: + /// **'OVERVIEW'** + String get rallyTitleOverview; + + /// The navigation link to the accounts page. + /// + /// In en, this message translates to: + /// **'ACCOUNTS'** + String get rallyTitleAccounts; + + /// The navigation link to the bills page. + /// + /// In en, this message translates to: + /// **'BILLS'** + String get rallyTitleBills; + + /// The navigation link to the budgets page. + /// + /// In en, this message translates to: + /// **'BUDGETS'** + String get rallyTitleBudgets; + + /// The navigation link to the settings page. + /// + /// In en, this message translates to: + /// **'SETTINGS'** + String get rallyTitleSettings; + + /// Title for login page for the Rally app (Rally does not need to be translated as it is a product name). + /// + /// In en, this message translates to: + /// **'Login to Rally'** + String get rallyLoginLoginToRally; + + /// Prompt for signing up for an account. + /// + /// In en, this message translates to: + /// **'Don\'t have an account?'** + String get rallyLoginNoAccount; + + /// Button text to sign up for an account. + /// + /// In en, this message translates to: + /// **'SIGN UP'** + String get rallyLoginSignUp; + + /// The username field in an login form. + /// + /// In en, this message translates to: + /// **'Username'** + String get rallyLoginUsername; + + /// The password field in an login form. + /// + /// In en, this message translates to: + /// **'Password'** + String get rallyLoginPassword; + + /// The label text to login. + /// + /// In en, this message translates to: + /// **'Login'** + String get rallyLoginLabelLogin; + + /// Text if the user wants to stay logged in. + /// + /// In en, this message translates to: + /// **'Remember Me'** + String get rallyLoginRememberMe; + + /// Text for login button. + /// + /// In en, this message translates to: + /// **'LOGIN'** + String get rallyLoginButtonLogin; + + /// Alert message shown when for example, user has used more than 90% of their shopping budget. + /// + /// In en, this message translates to: + /// **'Heads up, you\'ve used up {percent} of your Shopping budget for this month.'** + String rallyAlertsMessageHeadsUpShopping(Object percent); + + /// Alert message shown when for example, user has spent $120 on Restaurants this week. + /// + /// In en, this message translates to: + /// **'You\'ve spent {amount} on Restaurants this week.'** + String rallyAlertsMessageSpentOnRestaurants(Object amount); + + /// Alert message shown when for example, the user has spent $24 in ATM fees this month. + /// + /// In en, this message translates to: + /// **'You\'ve spent {amount} in ATM fees this month'** + String rallyAlertsMessageATMFees(Object amount); + + /// Alert message shown when for example, the checking account is 1% higher than last month. + /// + /// In en, this message translates to: + /// **'Good work! Your checking account is {percent} higher than last month.'** + String rallyAlertsMessageCheckingAccount(Object percent); + + /// Alert message shown when you have unassigned transactions. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{Increase your potential tax deduction! Assign categories to 1 unassigned transaction.}other{Increase your potential tax deduction! Assign categories to {count} unassigned transactions.}}'** + String rallyAlertsMessageUnassignedTransactions(num count); + + /// Semantics label for button to see all accounts. Accounts refer to bank account here. + /// + /// In en, this message translates to: + /// **'See all accounts'** + String get rallySeeAllAccounts; + + /// Semantics label for button to see all bills. + /// + /// In en, this message translates to: + /// **'See all bills'** + String get rallySeeAllBills; + + /// Semantics label for button to see all budgets. + /// + /// In en, this message translates to: + /// **'See all budgets'** + String get rallySeeAllBudgets; + + /// Semantics label for row with bank account name (for example checking) and its bank account number (for example 123), with how much money is deposited in it (for example $12). + /// + /// In en, this message translates to: + /// **'{accountName} account {accountNumber} with {amount}.'** + String rallyAccountAmount(Object accountName, Object accountNumber, Object amount); + + /// Semantics label for row with a bill (example name is rent), when the bill is due (1/12/2019 for example) and for how much money ($12). + /// + /// In en, this message translates to: + /// **'{billName} bill due {date} for {amount}.'** + String rallyBillAmount(Object billName, Object date, Object amount); + + /// Semantics label for row with a budget (housing budget for example), with how much is used of the budget (for example $5), the total budget (for example $100) and the amount left in the budget (for example $95). + /// + /// In en, this message translates to: + /// **'{budgetName} budget with {amountUsed} used of {amountTotal}, {amountLeft} left'** + String rallyBudgetAmount(Object budgetName, Object amountUsed, Object amountTotal, Object amountLeft); + + /// Study description for Crane. + /// + /// In en, this message translates to: + /// **'A personalized travel app'** + String get craneDescription; + + /// Category title on home screen for styles & other demos (for context, the styles demos consist of a color demo and a typography demo). + /// + /// In en, this message translates to: + /// **'STYLES & OTHER'** + String get homeCategoryReference; + + /// Error message when opening the URL for a demo. + /// + /// In en, this message translates to: + /// **'Couldn\'t display URL:'** + String get demoInvalidURL; + + /// Tooltip for options button in a demo. + /// + /// In en, this message translates to: + /// **'Options'** + String get demoOptionsTooltip; + + /// Tooltip for info button in a demo. + /// + /// In en, this message translates to: + /// **'Info'** + String get demoInfoTooltip; + + /// Tooltip for demo code button in a demo. + /// + /// In en, this message translates to: + /// **'Demo Code'** + String get demoCodeTooltip; + + /// Tooltip for API documentation button in a demo. + /// + /// In en, this message translates to: + /// **'API Documentation'** + String get demoDocumentationTooltip; + + /// Tooltip for Full Screen button in a demo. + /// + /// In en, this message translates to: + /// **'Full Screen'** + String get demoFullscreenTooltip; + + /// Caption for a button to copy all text. + /// + /// In en, this message translates to: + /// **'COPY ALL'** + String get demoCodeViewerCopyAll; + + /// A message displayed to the user after clicking the COPY ALL button, if the text is successfully copied to the clipboard. + /// + /// In en, this message translates to: + /// **'Copied to clipboard.'** + String get demoCodeViewerCopiedToClipboardMessage; + + /// A message displayed to the user after clicking the COPY ALL button, if the text CANNOT be copied to the clipboard. + /// + /// In en, this message translates to: + /// **'Failed to copy to clipboard: {error}'** + String demoCodeViewerFailedToCopyToClipboardMessage(Object error); + + /// Title for an alert that explains what the options button does. + /// + /// In en, this message translates to: + /// **'View options'** + String get demoOptionsFeatureTitle; + + /// Description for an alert that explains what the options button does. + /// + /// In en, this message translates to: + /// **'Tap here to view available options for this demo.'** + String get demoOptionsFeatureDescription; + + /// Title for the settings screen. + /// + /// In en, this message translates to: + /// **'Settings'** + String get settingsTitle; + + /// Accessibility label for the settings button when settings are not showing. + /// + /// In en, this message translates to: + /// **'Settings'** + String get settingsButtonLabel; + + /// Accessibility label for the settings button when settings are showing. + /// + /// In en, this message translates to: + /// **'Close settings'** + String get settingsButtonCloseLabel; + + /// Option label to indicate the system default will be used. + /// + /// In en, this message translates to: + /// **'System'** + String get settingsSystemDefault; + + /// Title for text scaling setting. + /// + /// In en, this message translates to: + /// **'Text scaling'** + String get settingsTextScaling; + + /// Option label for small text scale setting. + /// + /// In en, this message translates to: + /// **'Small'** + String get settingsTextScalingSmall; + + /// Option label for normal text scale setting. + /// + /// In en, this message translates to: + /// **'Normal'** + String get settingsTextScalingNormal; + + /// Option label for large text scale setting. + /// + /// In en, this message translates to: + /// **'Large'** + String get settingsTextScalingLarge; + + /// Option label for huge text scale setting. + /// + /// In en, this message translates to: + /// **'Huge'** + String get settingsTextScalingHuge; + + /// Title for text direction setting. + /// + /// In en, this message translates to: + /// **'Text direction'** + String get settingsTextDirection; + + /// Option label for locale-based text direction setting. + /// + /// In en, this message translates to: + /// **'Based on locale'** + String get settingsTextDirectionLocaleBased; + + /// Option label for left-to-right text direction setting. + /// + /// In en, this message translates to: + /// **'LTR'** + String get settingsTextDirectionLTR; + + /// Option label for right-to-left text direction setting. + /// + /// In en, this message translates to: + /// **'RTL'** + String get settingsTextDirectionRTL; + + /// Title for locale setting. + /// + /// In en, this message translates to: + /// **'Locale'** + String get settingsLocale; + + /// Title for platform mechanics (iOS, Android, macOS, etc.) setting. + /// + /// In en, this message translates to: + /// **'Platform mechanics'** + String get settingsPlatformMechanics; + + /// Title for the theme setting. + /// + /// In en, this message translates to: + /// **'Theme'** + String get settingsTheme; + + /// Title for the dark theme setting. + /// + /// In en, this message translates to: + /// **'Dark'** + String get settingsDarkTheme; + + /// Title for the light theme setting. + /// + /// In en, this message translates to: + /// **'Light'** + String get settingsLightTheme; + + /// Title for slow motion setting. + /// + /// In en, this message translates to: + /// **'Slow motion'** + String get settingsSlowMotion; + + /// Title for information button. + /// + /// In en, this message translates to: + /// **'About Flutter Gallery'** + String get settingsAbout; + + /// Title for feedback button. + /// + /// In en, this message translates to: + /// **'Send feedback'** + String get settingsFeedback; + + /// Title for attribution (TOASTER is a proper name and should remain in English). + /// + /// In en, this message translates to: + /// **'Designed by TOASTER in London'** + String get settingsAttribution; + + /// Title for the material App bar component demo. + /// + /// In en, this message translates to: + /// **'App bar'** + String get demoAppBarTitle; + + /// Subtitle for the material App bar component demo. + /// + /// In en, this message translates to: + /// **'Displays information and actions relating to the current screen'** + String get demoAppBarSubtitle; + + /// Description for the material App bar component demo. + /// + /// In en, this message translates to: + /// **'The App bar provides content and actions related to the current screen. It\'s used for branding, screen titles, navigation, and actions'** + String get demoAppBarDescription; + + /// Title for the material bottom app bar component demo. + /// + /// In en, this message translates to: + /// **'Bottom app bar'** + String get demoBottomAppBarTitle; + + /// Subtitle for the material bottom app bar component demo. + /// + /// In en, this message translates to: + /// **'Displays navigation and actions at the bottom'** + String get demoBottomAppBarSubtitle; + + /// Description for the material bottom app bar component demo. + /// + /// In en, this message translates to: + /// **'Bottom app bars provide access to a bottom navigation drawer and up to four actions, including the floating action button.'** + String get demoBottomAppBarDescription; + + /// A toggle for whether to have a notch (or cutout) in the bottom app bar demo. + /// + /// In en, this message translates to: + /// **'Notch'** + String get bottomAppBarNotch; + + /// A setting for the position of the floating action button in the bottom app bar demo. + /// + /// In en, this message translates to: + /// **'Floating Action Button Position'** + String get bottomAppBarPosition; + + /// A setting for the position of the floating action button in the bottom app bar that docks the button in the bar and aligns it at the end. + /// + /// In en, this message translates to: + /// **'Docked - End'** + String get bottomAppBarPositionDockedEnd; + + /// A setting for the position of the floating action button in the bottom app bar that docks the button in the bar and aligns it in the center. + /// + /// In en, this message translates to: + /// **'Docked - Center'** + String get bottomAppBarPositionDockedCenter; + + /// A setting for the position of the floating action button in the bottom app bar that places the button above the bar and aligns it at the end. + /// + /// In en, this message translates to: + /// **'Floating - End'** + String get bottomAppBarPositionFloatingEnd; + + /// A setting for the position of the floating action button in the bottom app bar that places the button above the bar and aligns it in the center. + /// + /// In en, this message translates to: + /// **'Floating - Center'** + String get bottomAppBarPositionFloatingCenter; + + /// Title for the material banner component demo. + /// + /// In en, this message translates to: + /// **'Banner'** + String get demoBannerTitle; + + /// Subtitle for the material banner component demo. + /// + /// In en, this message translates to: + /// **'Displaying a banner within a list'** + String get demoBannerSubtitle; + + /// Description for the material banner component demo. + /// + /// In en, this message translates to: + /// **'A banner displays an important, succinct message, and provides actions for users to address (or dismiss the banner). A user action is required for it to be dismissed.'** + String get demoBannerDescription; + + /// Title for the material bottom navigation component demo. + /// + /// In en, this message translates to: + /// **'Bottom navigation'** + String get demoBottomNavigationTitle; + + /// Subtitle for the material bottom navigation component demo. + /// + /// In en, this message translates to: + /// **'Bottom navigation with cross-fading views'** + String get demoBottomNavigationSubtitle; + + /// Option title for bottom navigation with persistent labels. + /// + /// In en, this message translates to: + /// **'Persistent labels'** + String get demoBottomNavigationPersistentLabels; + + /// Option title for bottom navigation with only a selected label. + /// + /// In en, this message translates to: + /// **'Selected label'** + String get demoBottomNavigationSelectedLabel; + + /// Description for the material bottom navigation component demo. + /// + /// In en, this message translates to: + /// **'Bottom navigation bars display three to five destinations at the bottom of a screen. Each destination is represented by an icon and an optional text label. When a bottom navigation icon is tapped, the user is taken to the top-level navigation destination associated with that icon.'** + String get demoBottomNavigationDescription; + + /// Title for the material buttons component demo. + /// + /// In en, this message translates to: + /// **'Buttons'** + String get demoButtonTitle; + + /// Subtitle for the material buttons component demo. + /// + /// In en, this message translates to: + /// **'Text, elevated, outlined, and more'** + String get demoButtonSubtitle; + + /// Title for the text button component demo. + /// + /// In en, this message translates to: + /// **'Text Button'** + String get demoTextButtonTitle; + + /// Description for the text button component demo. + /// + /// In en, this message translates to: + /// **'A text button displays an ink splash on press but does not lift. Use text buttons on toolbars, in dialogs and inline with padding'** + String get demoTextButtonDescription; + + /// Title for the elevated button component demo. + /// + /// In en, this message translates to: + /// **'Elevated Button'** + String get demoElevatedButtonTitle; + + /// Description for the elevated button component demo. + /// + /// In en, this message translates to: + /// **'Elevated buttons add dimension to mostly flat layouts. They emphasize functions on busy or wide spaces.'** + String get demoElevatedButtonDescription; + + /// Title for the outlined button component demo. + /// + /// In en, this message translates to: + /// **'Outlined Button'** + String get demoOutlinedButtonTitle; + + /// Description for the outlined button component demo. + /// + /// In en, this message translates to: + /// **'Outlined buttons become opaque and elevate when pressed. They are often paired with raised buttons to indicate an alternative, secondary action.'** + String get demoOutlinedButtonDescription; + + /// Title for the toggle buttons component demo. + /// + /// In en, this message translates to: + /// **'Toggle Buttons'** + String get demoToggleButtonTitle; + + /// Description for the toggle buttons component demo. + /// + /// In en, this message translates to: + /// **'Toggle buttons can be used to group related options. To emphasize groups of related toggle buttons, a group should share a common container'** + String get demoToggleButtonDescription; + + /// Title for the floating action button component demo. + /// + /// In en, this message translates to: + /// **'Floating Action Button'** + String get demoFloatingButtonTitle; + + /// Description for the floating action button component demo. + /// + /// In en, this message translates to: + /// **'A floating action button is a circular icon button that hovers over content to promote a primary action in the application.'** + String get demoFloatingButtonDescription; + + /// Title for the material cards component demo. + /// + /// In en, this message translates to: + /// **'Cards'** + String get demoCardTitle; + + /// Subtitle for the material cards component demo. + /// + /// In en, this message translates to: + /// **'Baseline cards with rounded corners'** + String get demoCardSubtitle; + + /// Title for the material chips component demo. + /// + /// In en, this message translates to: + /// **'Chips'** + String get demoChipTitle; + + /// Description for the material cards component demo. + /// + /// In en, this message translates to: + /// **'A card is a sheet of Material used to represent some related information, for example an album, a geographical location, a meal, contact details, etc.'** + String get demoCardDescription; + + /// Subtitle for the material chips component demo. + /// + /// In en, this message translates to: + /// **'Compact elements that represent an input, attribute, or action'** + String get demoChipSubtitle; + + /// Title for the action chip component demo. + /// + /// In en, this message translates to: + /// **'Action Chip'** + String get demoActionChipTitle; + + /// Description for the action chip component demo. + /// + /// In en, this message translates to: + /// **'Action chips are a set of options which trigger an action related to primary content. Action chips should appear dynamically and contextually in a UI.'** + String get demoActionChipDescription; + + /// Title for the choice chip component demo. + /// + /// In en, this message translates to: + /// **'Choice Chip'** + String get demoChoiceChipTitle; + + /// Description for the choice chip component demo. + /// + /// In en, this message translates to: + /// **'Choice chips represent a single choice from a set. Choice chips contain related descriptive text or categories.'** + String get demoChoiceChipDescription; + + /// Title for the filter chip component demo. + /// + /// In en, this message translates to: + /// **'Filter Chip'** + String get demoFilterChipTitle; + + /// Description for the filter chip component demo. + /// + /// In en, this message translates to: + /// **'Filter chips use tags or descriptive words as a way to filter content.'** + String get demoFilterChipDescription; + + /// Title for the input chip component demo. + /// + /// In en, this message translates to: + /// **'Input Chip'** + String get demoInputChipTitle; + + /// Description for the input chip component demo. + /// + /// In en, this message translates to: + /// **'Input chips represent a complex piece of information, such as an entity (person, place, or thing) or conversational text, in a compact form.'** + String get demoInputChipDescription; + + /// Title for the material data table component demo. + /// + /// In en, this message translates to: + /// **'Data Tables'** + String get demoDataTableTitle; + + /// Subtitle for the material data table component demo. + /// + /// In en, this message translates to: + /// **'Rows and columns of information'** + String get demoDataTableSubtitle; + + /// Description for the material data table component demo. + /// + /// In en, this message translates to: + /// **'Data tables display information in a grid-like format of rows and columns. They organize information in a way that\'s easy to scan, so that users can look for patterns and insights.'** + String get demoDataTableDescription; + + /// Header for the data table component demo about nutrition. + /// + /// In en, this message translates to: + /// **'Nutrition'** + String get dataTableHeader; + + /// Column header for desserts. + /// + /// In en, this message translates to: + /// **'Dessert (1 serving)'** + String get dataTableColumnDessert; + + /// Column header for number of calories. + /// + /// In en, this message translates to: + /// **'Calories'** + String get dataTableColumnCalories; + + /// Column header for number of grams of fat. + /// + /// In en, this message translates to: + /// **'Fat (g)'** + String get dataTableColumnFat; + + /// Column header for number of grams of carbs. + /// + /// In en, this message translates to: + /// **'Carbs (g)'** + String get dataTableColumnCarbs; + + /// Column header for number of grams of protein. + /// + /// In en, this message translates to: + /// **'Protein (g)'** + String get dataTableColumnProtein; + + /// Column header for number of milligrams of sodium. + /// + /// In en, this message translates to: + /// **'Sodium (mg)'** + String get dataTableColumnSodium; + + /// Column header for daily percentage of calcium. + /// + /// In en, this message translates to: + /// **'Calcium (%)'** + String get dataTableColumnCalcium; + + /// Column header for daily percentage of iron. + /// + /// In en, this message translates to: + /// **'Iron (%)'** + String get dataTableColumnIron; + + /// Column row for frozen yogurt. + /// + /// In en, this message translates to: + /// **'Frozen yogurt'** + String get dataTableRowFrozenYogurt; + + /// Column row for Ice cream sandwich. + /// + /// In en, this message translates to: + /// **'Ice cream sandwich'** + String get dataTableRowIceCreamSandwich; + + /// Column row for Eclair. + /// + /// In en, this message translates to: + /// **'Eclair'** + String get dataTableRowEclair; + + /// Column row for Cupcake. + /// + /// In en, this message translates to: + /// **'Cupcake'** + String get dataTableRowCupcake; + + /// Column row for Gingerbread. + /// + /// In en, this message translates to: + /// **'Gingerbread'** + String get dataTableRowGingerbread; + + /// Column row for Jelly bean. + /// + /// In en, this message translates to: + /// **'Jelly bean'** + String get dataTableRowJellyBean; + + /// Column row for Lollipop. + /// + /// In en, this message translates to: + /// **'Lollipop'** + String get dataTableRowLollipop; + + /// Column row for Honeycomb. + /// + /// In en, this message translates to: + /// **'Honeycomb'** + String get dataTableRowHoneycomb; + + /// Column row for Donut. + /// + /// In en, this message translates to: + /// **'Donut'** + String get dataTableRowDonut; + + /// Column row for Apple pie. + /// + /// In en, this message translates to: + /// **'Apple pie'** + String get dataTableRowApplePie; + + /// A dessert with sugar on it. The parameter is some type of dessert. + /// + /// In en, this message translates to: + /// **'{value} with sugar'** + String dataTableRowWithSugar(Object value); + + /// A dessert with honey on it. The parameter is some type of dessert. + /// + /// In en, this message translates to: + /// **'{value} with honey'** + String dataTableRowWithHoney(Object value); + + /// Title for the material dialog component demo. + /// + /// In en, this message translates to: + /// **'Dialogs'** + String get demoDialogTitle; + + /// Subtitle for the material dialog component demo. + /// + /// In en, this message translates to: + /// **'Simple, alert, and fullscreen'** + String get demoDialogSubtitle; + + /// Title for the alert dialog component demo. + /// + /// In en, this message translates to: + /// **'Alert'** + String get demoAlertDialogTitle; + + /// Description for the alert dialog component demo. + /// + /// In en, this message translates to: + /// **'An alert dialog informs the user about situations that require acknowledgement. An alert dialog has an optional title and an optional list of actions.'** + String get demoAlertDialogDescription; + + /// Title for the alert dialog with title component demo. + /// + /// In en, this message translates to: + /// **'Alert With Title'** + String get demoAlertTitleDialogTitle; + + /// Title for the simple dialog component demo. + /// + /// In en, this message translates to: + /// **'Simple'** + String get demoSimpleDialogTitle; + + /// Description for the simple dialog component demo. + /// + /// In en, this message translates to: + /// **'A simple dialog offers the user a choice between several options. A simple dialog has an optional title that is displayed above the choices.'** + String get demoSimpleDialogDescription; + + /// Title for the divider component demo. + /// + /// In en, this message translates to: + /// **'Divider'** + String get demoDividerTitle; + + /// Subtitle for the divider component demo. + /// + /// In en, this message translates to: + /// **'A divider is a thin line that groups content in lists and layouts.'** + String get demoDividerSubtitle; + + /// Description for the divider component demo. + /// + /// In en, this message translates to: + /// **'Dividers can be used in lists, drawers, and elsewhere to separate content.'** + String get demoDividerDescription; + + /// Title for the vertical divider component demo. + /// + /// In en, this message translates to: + /// **'Vertical Divider'** + String get demoVerticalDividerTitle; + + /// Title for the grid lists component demo. + /// + /// In en, this message translates to: + /// **'Grid Lists'** + String get demoGridListsTitle; + + /// Subtitle for the grid lists component demo. + /// + /// In en, this message translates to: + /// **'Row and column layout'** + String get demoGridListsSubtitle; + + /// Description for the grid lists component demo. + /// + /// In en, this message translates to: + /// **'Grid Lists are best suited for presenting homogeneous data, typically images. Each item in a grid list is called a tile.'** + String get demoGridListsDescription; + + /// Title for the grid lists image-only component demo. + /// + /// In en, this message translates to: + /// **'Image only'** + String get demoGridListsImageOnlyTitle; + + /// Title for the grid lists component demo with headers on each tile. + /// + /// In en, this message translates to: + /// **'With header'** + String get demoGridListsHeaderTitle; + + /// Title for the grid lists component demo with footers on each tile. + /// + /// In en, this message translates to: + /// **'With footer'** + String get demoGridListsFooterTitle; + + /// Title for the sliders component demo. + /// + /// In en, this message translates to: + /// **'Sliders'** + String get demoSlidersTitle; + + /// Short description for the sliders component demo. + /// + /// In en, this message translates to: + /// **'Widgets for selecting a value by swiping'** + String get demoSlidersSubtitle; + + /// Description for the sliders demo. + /// + /// In en, this message translates to: + /// **'Sliders reflect a range of values along a bar, from which users may select a single value. They are ideal for adjusting settings such as volume, brightness, or applying image filters.'** + String get demoSlidersDescription; + + /// Title for the range sliders component demo. + /// + /// In en, this message translates to: + /// **'Range Sliders'** + String get demoRangeSlidersTitle; + + /// Description for the range sliders demo. + /// + /// In en, this message translates to: + /// **'Sliders reflect a range of values along a bar. They can have icons on both ends of the bar that reflect a range of values. They are ideal for adjusting settings such as volume, brightness, or applying image filters.'** + String get demoRangeSlidersDescription; + + /// Title for the custom sliders component demo. + /// + /// In en, this message translates to: + /// **'Custom Sliders'** + String get demoCustomSlidersTitle; + + /// Description for the custom sliders demo. + /// + /// In en, this message translates to: + /// **'Sliders reflect a range of values along a bar, from which users may select a single value or range of values. The sliders can be themed and customized.'** + String get demoCustomSlidersDescription; + + /// Text to describe a slider has a continuous value with an editable numerical value. + /// + /// In en, this message translates to: + /// **'Continuous with Editable Numerical Value'** + String get demoSlidersContinuousWithEditableNumericalValue; + + /// Text to describe that we have a slider with discrete values. + /// + /// In en, this message translates to: + /// **'Discrete'** + String get demoSlidersDiscrete; + + /// Text to describe that we have a slider with discrete values and a custom theme. + /// + /// In en, this message translates to: + /// **'Discrete Slider with Custom Theme'** + String get demoSlidersDiscreteSliderWithCustomTheme; + + /// Text to describe that we have a range slider with continuous values and a custom theme. + /// + /// In en, this message translates to: + /// **'Continuous Range Slider with Custom Theme'** + String get demoSlidersContinuousRangeSliderWithCustomTheme; + + /// Text to describe that we have a slider with continuous values. + /// + /// In en, this message translates to: + /// **'Continuous'** + String get demoSlidersContinuous; + + /// Label for input field that has an editable numerical value. + /// + /// In en, this message translates to: + /// **'Editable numerical value'** + String get demoSlidersEditableNumericalValue; + + /// Title for the menu component demo. + /// + /// In en, this message translates to: + /// **'Menu'** + String get demoMenuTitle; + + /// Title for the context menu component demo. + /// + /// In en, this message translates to: + /// **'Context menu'** + String get demoContextMenuTitle; + + /// Title for the sectioned menu component demo. + /// + /// In en, this message translates to: + /// **'Sectioned menu'** + String get demoSectionedMenuTitle; + + /// Title for the simple menu component demo. + /// + /// In en, this message translates to: + /// **'Simple menu'** + String get demoSimpleMenuTitle; + + /// Title for the checklist menu component demo. + /// + /// In en, this message translates to: + /// **'Checklist menu'** + String get demoChecklistMenuTitle; + + /// Short description for the menu component demo. + /// + /// In en, this message translates to: + /// **'Menu buttons and simple menus'** + String get demoMenuSubtitle; + + /// Description for the menu demo. + /// + /// In en, this message translates to: + /// **'A menu displays a list of choices on a temporary surface. They appear when users interact with a button, action, or other control.'** + String get demoMenuDescription; + + /// The first item in a menu. + /// + /// In en, this message translates to: + /// **'Menu item one'** + String get demoMenuItemValueOne; + + /// The second item in a menu. + /// + /// In en, this message translates to: + /// **'Menu item two'** + String get demoMenuItemValueTwo; + + /// The third item in a menu. + /// + /// In en, this message translates to: + /// **'Menu item three'** + String get demoMenuItemValueThree; + + /// The number one. + /// + /// In en, this message translates to: + /// **'One'** + String get demoMenuOne; + + /// The number two. + /// + /// In en, this message translates to: + /// **'Two'** + String get demoMenuTwo; + + /// The number three. + /// + /// In en, this message translates to: + /// **'Three'** + String get demoMenuThree; + + /// The number four. + /// + /// In en, this message translates to: + /// **'Four'** + String get demoMenuFour; + + /// Label next to a button that opens a menu. A menu displays a list of choices on a temporary surface. Used as an example in a demo. + /// + /// In en, this message translates to: + /// **'An item with a context menu'** + String get demoMenuAnItemWithAContextMenuButton; + + /// Text label for a context menu item. A menu displays a list of choices on a temporary surface. Used as an example in a demo. + /// + /// In en, this message translates to: + /// **'Context menu item one'** + String get demoMenuContextMenuItemOne; + + /// Text label for a disabled menu item. A menu displays a list of choices on a temporary surface. Used as an example in a demo. + /// + /// In en, this message translates to: + /// **'Disabled menu item'** + String get demoMenuADisabledMenuItem; + + /// Text label for a context menu item three. A menu displays a list of choices on a temporary surface. Used as an example in a demo. + /// + /// In en, this message translates to: + /// **'Context menu item three'** + String get demoMenuContextMenuItemThree; + + /// Label next to a button that opens a sectioned menu . A menu displays a list of choices on a temporary surface. Used as an example in a demo. + /// + /// In en, this message translates to: + /// **'An item with a sectioned menu'** + String get demoMenuAnItemWithASectionedMenu; + + /// Button to preview content. + /// + /// In en, this message translates to: + /// **'Preview'** + String get demoMenuPreview; + + /// Button to share content. + /// + /// In en, this message translates to: + /// **'Share'** + String get demoMenuShare; + + /// Button to get link for content. + /// + /// In en, this message translates to: + /// **'Get link'** + String get demoMenuGetLink; + + /// Button to remove content. + /// + /// In en, this message translates to: + /// **'Remove'** + String get demoMenuRemove; + + /// A text to show what value was selected. + /// + /// In en, this message translates to: + /// **'Selected: {value}'** + String demoMenuSelected(Object value); + + /// A text to show what value was checked. + /// + /// In en, this message translates to: + /// **'Checked: {value}'** + String demoMenuChecked(Object value); + + /// Title for the material drawer component demo. + /// + /// In en, this message translates to: + /// **'Navigation Drawer'** + String get demoNavigationDrawerTitle; + + /// Subtitle for the material drawer component demo. + /// + /// In en, this message translates to: + /// **'Displaying a drawer within appbar'** + String get demoNavigationDrawerSubtitle; + + /// Description for the material drawer component demo. + /// + /// In en, this message translates to: + /// **'A Material Design panel that slides in horizontally from the edge of the screen to show navigation links in an application.'** + String get demoNavigationDrawerDescription; + + /// Demo username for navigation drawer. + /// + /// In en, this message translates to: + /// **'User Name'** + String get demoNavigationDrawerUserName; + + /// Demo email for navigation drawer. + /// + /// In en, this message translates to: + /// **'user.name@example.com'** + String get demoNavigationDrawerUserEmail; + + /// Drawer Item One. + /// + /// In en, this message translates to: + /// **'Item One'** + String get demoNavigationDrawerToPageOne; + + /// Drawer Item Two. + /// + /// In en, this message translates to: + /// **'Item Two'** + String get demoNavigationDrawerToPageTwo; + + /// Description to open navigation drawer. + /// + /// In en, this message translates to: + /// **'Swipe from the edge or tap the upper-left icon to see the drawer'** + String get demoNavigationDrawerText; + + /// Title for the material Navigation Rail component demo. + /// + /// In en, this message translates to: + /// **'Navigation Rail'** + String get demoNavigationRailTitle; + + /// Subtitle for the material Navigation Rail component demo. + /// + /// In en, this message translates to: + /// **'Displaying a Navigation Rail within an app'** + String get demoNavigationRailSubtitle; + + /// Description for the material Navigation Rail component demo. + /// + /// In en, this message translates to: + /// **'A material widget that is meant to be displayed at the left or right of an app to navigate between a small number of views, typically between three and five.'** + String get demoNavigationRailDescription; + + /// Navigation Rail destination first label. + /// + /// In en, this message translates to: + /// **'First'** + String get demoNavigationRailFirst; + + /// Navigation Rail destination second label. + /// + /// In en, this message translates to: + /// **'Second'** + String get demoNavigationRailSecond; + + /// Navigation Rail destination Third label. + /// + /// In en, this message translates to: + /// **'Third'** + String get demoNavigationRailThird; + + /// Label next to a button that opens a simple menu. A menu displays a list of choices on a temporary surface. Used as an example in a demo. + /// + /// In en, this message translates to: + /// **'An item with a simple menu'** + String get demoMenuAnItemWithASimpleMenu; + + /// Label next to a button that opens a checklist menu. A menu displays a list of choices on a temporary surface. Used as an example in a demo. + /// + /// In en, this message translates to: + /// **'An item with a checklist menu'** + String get demoMenuAnItemWithAChecklistMenu; + + /// Title for the fullscreen dialog component demo. + /// + /// In en, this message translates to: + /// **'Fullscreen'** + String get demoFullscreenDialogTitle; + + /// Description for the fullscreen dialog component demo. + /// + /// In en, this message translates to: + /// **'The fullscreenDialog property specifies whether the incoming page is a fullscreen modal dialog'** + String get demoFullscreenDialogDescription; + + /// Title for the cupertino activity indicator component demo. + /// + /// In en, this message translates to: + /// **'Activity indicator'** + String get demoCupertinoActivityIndicatorTitle; + + /// Subtitle for the cupertino activity indicator component demo. + /// + /// In en, this message translates to: + /// **'iOS-style activity indicators'** + String get demoCupertinoActivityIndicatorSubtitle; + + /// Description for the cupertino activity indicator component demo. + /// + /// In en, this message translates to: + /// **'An iOS-style activity indicator that spins clockwise.'** + String get demoCupertinoActivityIndicatorDescription; + + /// Title for the cupertino buttons component demo. + /// + /// In en, this message translates to: + /// **'Buttons'** + String get demoCupertinoButtonsTitle; + + /// Subtitle for the cupertino buttons component demo. + /// + /// In en, this message translates to: + /// **'iOS-style buttons'** + String get demoCupertinoButtonsSubtitle; + + /// Description for the cupertino buttons component demo. + /// + /// In en, this message translates to: + /// **'An iOS-style button. It takes in text and/or an icon that fades out and in on touch. May optionally have a background.'** + String get demoCupertinoButtonsDescription; + + /// Title for the cupertino context menu component demo. + /// + /// In en, this message translates to: + /// **'Context Menu'** + String get demoCupertinoContextMenuTitle; + + /// Subtitle for the cupertino context menu component demo. + /// + /// In en, this message translates to: + /// **'iOS-style context menu'** + String get demoCupertinoContextMenuSubtitle; + + /// Description for the cupertino context menu component demo. + /// + /// In en, this message translates to: + /// **'An iOS-style full screen contextual menu that appears when an element is long-pressed.'** + String get demoCupertinoContextMenuDescription; + + /// Context menu list item one + /// + /// In en, this message translates to: + /// **'Action one'** + String get demoCupertinoContextMenuActionOne; + + /// Context menu list item two + /// + /// In en, this message translates to: + /// **'Action two'** + String get demoCupertinoContextMenuActionTwo; + + /// Context menu text. + /// + /// In en, this message translates to: + /// **'Tap and hold the Flutter logo to see the context menu.'** + String get demoCupertinoContextMenuActionText; + + /// Title for the cupertino alerts component demo. + /// + /// In en, this message translates to: + /// **'Alerts'** + String get demoCupertinoAlertsTitle; + + /// Subtitle for the cupertino alerts component demo. + /// + /// In en, this message translates to: + /// **'iOS-style alert dialogs'** + String get demoCupertinoAlertsSubtitle; + + /// Title for the cupertino alert component demo. + /// + /// In en, this message translates to: + /// **'Alert'** + String get demoCupertinoAlertTitle; + + /// Description for the cupertino alert component demo. + /// + /// In en, this message translates to: + /// **'An alert dialog informs the user about situations that require acknowledgement. An alert dialog has an optional title, optional content, and an optional list of actions. The title is displayed above the content and the actions are displayed below the content.'** + String get demoCupertinoAlertDescription; + + /// Title for the cupertino alert with title component demo. + /// + /// In en, this message translates to: + /// **'Alert With Title'** + String get demoCupertinoAlertWithTitleTitle; + + /// Title for the cupertino alert with buttons component demo. + /// + /// In en, this message translates to: + /// **'Alert With Buttons'** + String get demoCupertinoAlertButtonsTitle; + + /// Title for the cupertino alert buttons only component demo. + /// + /// In en, this message translates to: + /// **'Alert Buttons Only'** + String get demoCupertinoAlertButtonsOnlyTitle; + + /// Title for the cupertino action sheet component demo. + /// + /// In en, this message translates to: + /// **'Action Sheet'** + String get demoCupertinoActionSheetTitle; + + /// Description for the cupertino action sheet component demo. + /// + /// In en, this message translates to: + /// **'An action sheet is a specific style of alert that presents the user with a set of two or more choices related to the current context. An action sheet can have a title, an additional message, and a list of actions.'** + String get demoCupertinoActionSheetDescription; + + /// Title for the cupertino navigation bar component demo. + /// + /// In en, this message translates to: + /// **'Navigation bar'** + String get demoCupertinoNavigationBarTitle; + + /// Subtitle for the cupertino navigation bar component demo. + /// + /// In en, this message translates to: + /// **'iOS-style navigation bar'** + String get demoCupertinoNavigationBarSubtitle; + + /// Description for the cupertino navigation bar component demo. + /// + /// In en, this message translates to: + /// **'An iOS-styled navigation bar. The navigation bar is a toolbar that minimally consists of a page title, in the middle of the toolbar.'** + String get demoCupertinoNavigationBarDescription; + + /// Title for the cupertino pickers component demo. + /// + /// In en, this message translates to: + /// **'Pickers'** + String get demoCupertinoPickerTitle; + + /// Subtitle for the cupertino pickers component demo. + /// + /// In en, this message translates to: + /// **'iOS-style pickers'** + String get demoCupertinoPickerSubtitle; + + /// Description for the cupertino pickers component demo. + /// + /// In en, this message translates to: + /// **'An iOS-style picker widget that can be used to select strings, dates, times, or both date and time.'** + String get demoCupertinoPickerDescription; + + /// Label to open a countdown timer picker. + /// + /// In en, this message translates to: + /// **'Timer'** + String get demoCupertinoPickerTimer; + + /// Label to open an iOS picker. + /// + /// In en, this message translates to: + /// **'Picker'** + String get demoCupertinoPicker; + + /// Label to open a date picker. + /// + /// In en, this message translates to: + /// **'Date'** + String get demoCupertinoPickerDate; + + /// Label to open a time picker. + /// + /// In en, this message translates to: + /// **'Time'** + String get demoCupertinoPickerTime; + + /// Label to open a date and time picker. + /// + /// In en, this message translates to: + /// **'Date and Time'** + String get demoCupertinoPickerDateTime; + + /// Title for the cupertino pull-to-refresh component demo. + /// + /// In en, this message translates to: + /// **'Pull to refresh'** + String get demoCupertinoPullToRefreshTitle; + + /// Subtitle for the cupertino pull-to-refresh component demo. + /// + /// In en, this message translates to: + /// **'iOS-style pull to refresh control'** + String get demoCupertinoPullToRefreshSubtitle; + + /// Description for the cupertino pull-to-refresh component demo. + /// + /// In en, this message translates to: + /// **'A widget implementing the iOS-style pull to refresh content control.'** + String get demoCupertinoPullToRefreshDescription; + + /// Title for the cupertino segmented control component demo. + /// + /// In en, this message translates to: + /// **'Segmented control'** + String get demoCupertinoSegmentedControlTitle; + + /// Subtitle for the cupertino segmented control component demo. + /// + /// In en, this message translates to: + /// **'iOS-style segmented control'** + String get demoCupertinoSegmentedControlSubtitle; + + /// Description for the cupertino segmented control component demo. + /// + /// In en, this message translates to: + /// **'Used to select between a number of mutually exclusive options. When one option in the segmented control is selected, the other options in the segmented control cease to be selected.'** + String get demoCupertinoSegmentedControlDescription; + + /// Title for the cupertino slider component demo. + /// + /// In en, this message translates to: + /// **'Slider'** + String get demoCupertinoSliderTitle; + + /// Subtitle for the cupertino slider component demo. + /// + /// In en, this message translates to: + /// **'iOS-style slider'** + String get demoCupertinoSliderSubtitle; + + /// Description for the cupertino slider component demo. + /// + /// In en, this message translates to: + /// **'A slider can be used to select from either a continuous or a discrete set of values.'** + String get demoCupertinoSliderDescription; + + /// A label for a continuous slider that indicates what value it is set to. + /// + /// In en, this message translates to: + /// **'Continuous: {value}'** + String demoCupertinoSliderContinuous(Object value); + + /// A label for a discrete slider that indicates what value it is set to. + /// + /// In en, this message translates to: + /// **'Discrete: {value}'** + String demoCupertinoSliderDiscrete(Object value); + + /// Subtitle for the cupertino switch component demo. + /// + /// In en, this message translates to: + /// **'iOS-style switch'** + String get demoCupertinoSwitchSubtitle; + + /// Description for the cupertino switch component demo. + /// + /// In en, this message translates to: + /// **'A switch is used to toggle the on/off state of a single setting.'** + String get demoCupertinoSwitchDescription; + + /// Title for the cupertino bottom tab bar demo. + /// + /// In en, this message translates to: + /// **'Tab bar'** + String get demoCupertinoTabBarTitle; + + /// Subtitle for the cupertino bottom tab bar demo. + /// + /// In en, this message translates to: + /// **'iOS-style bottom tab bar'** + String get demoCupertinoTabBarSubtitle; + + /// Description for the cupertino bottom tab bar demo. + /// + /// In en, this message translates to: + /// **'An iOS-style bottom navigation tab bar. Displays multiple tabs with one tab being active, the first tab by default.'** + String get demoCupertinoTabBarDescription; + + /// Title for the home tab in the bottom tab bar demo. + /// + /// In en, this message translates to: + /// **'Home'** + String get cupertinoTabBarHomeTab; + + /// Title for the chat tab in the bottom tab bar demo. + /// + /// In en, this message translates to: + /// **'Chat'** + String get cupertinoTabBarChatTab; + + /// Title for the profile tab in the bottom tab bar demo. + /// + /// In en, this message translates to: + /// **'Profile'** + String get cupertinoTabBarProfileTab; + + /// Title for the cupertino text field demo. + /// + /// In en, this message translates to: + /// **'Text fields'** + String get demoCupertinoTextFieldTitle; + + /// Subtitle for the cupertino text field demo. + /// + /// In en, this message translates to: + /// **'iOS-style text fields'** + String get demoCupertinoTextFieldSubtitle; + + /// Description for the cupertino text field demo. + /// + /// In en, this message translates to: + /// **'A text field lets the user enter text, either with a hardware keyboard or with an onscreen keyboard.'** + String get demoCupertinoTextFieldDescription; + + /// The placeholder for a text field where a user would enter their PIN number. + /// + /// In en, this message translates to: + /// **'PIN'** + String get demoCupertinoTextFieldPIN; + + /// Title for the cupertino search text field demo. + /// + /// In en, this message translates to: + /// **'Search text field'** + String get demoCupertinoSearchTextFieldTitle; + + /// Subtitle for the cupertino search text field demo. + /// + /// In en, this message translates to: + /// **'iOS-style search text field'** + String get demoCupertinoSearchTextFieldSubtitle; + + /// Description for the cupertino search text field demo. + /// + /// In en, this message translates to: + /// **'A search text field that lets the user search by entering text, and that can offer and filter suggestions.'** + String get demoCupertinoSearchTextFieldDescription; + + /// The placeholder for a search text field demo. + /// + /// In en, this message translates to: + /// **'Enter some text'** + String get demoCupertinoSearchTextFieldPlaceholder; + + /// Title for the cupertino scrollbar demo. + /// + /// In en, this message translates to: + /// **'Scrollbar'** + String get demoCupertinoScrollbarTitle; + + /// Subtitle for the cupertino scrollbar demo. + /// + /// In en, this message translates to: + /// **'iOS-style scrollbar'** + String get demoCupertinoScrollbarSubtitle; + + /// Description for the cupertino scrollbar demo. + /// + /// In en, this message translates to: + /// **'A scrollbar that wraps the given child'** + String get demoCupertinoScrollbarDescription; + + /// Title for the motion demo. + /// + /// In en, this message translates to: + /// **'Motion'** + String get demoMotionTitle; + + /// Subtitle for the motion demo. + /// + /// In en, this message translates to: + /// **'All of the predefined transition patterns'** + String get demoMotionSubtitle; + + /// Instructions for the container transform demo located in the app bar. + /// + /// In en, this message translates to: + /// **'Cards, Lists & FAB'** + String get demoContainerTransformDemoInstructions; + + /// Instructions for the shared x axis demo located in the app bar. + /// + /// In en, this message translates to: + /// **'Next and Back Buttons'** + String get demoSharedXAxisDemoInstructions; + + /// Instructions for the shared y axis demo located in the app bar. + /// + /// In en, this message translates to: + /// **'Sort by \"Recently Played\"'** + String get demoSharedYAxisDemoInstructions; + + /// Instructions for the shared z axis demo located in the app bar. + /// + /// In en, this message translates to: + /// **'Settings icon button'** + String get demoSharedZAxisDemoInstructions; + + /// Instructions for the fade through demo located in the app bar. + /// + /// In en, this message translates to: + /// **'Bottom navigation'** + String get demoFadeThroughDemoInstructions; + + /// Instructions for the fade scale demo located in the app bar. + /// + /// In en, this message translates to: + /// **'Modal and FAB'** + String get demoFadeScaleDemoInstructions; + + /// Title for the container transform demo. + /// + /// In en, this message translates to: + /// **'Container Transform'** + String get demoContainerTransformTitle; + + /// Description for the container transform demo. + /// + /// In en, this message translates to: + /// **'The container transform pattern is designed for transitions between UI elements that include a container. This pattern creates a visible connection between two UI elements'** + String get demoContainerTransformDescription; + + /// Title for the container transform modal bottom sheet. + /// + /// In en, this message translates to: + /// **'Fade mode'** + String get demoContainerTransformModalBottomSheetTitle; + + /// Description for container transform fade type setting. + /// + /// In en, this message translates to: + /// **'FADE'** + String get demoContainerTransformTypeFade; + + /// Description for container transform fade through type setting. + /// + /// In en, this message translates to: + /// **'FADE THROUGH'** + String get demoContainerTransformTypeFadeThrough; + + /// The placeholder for the motion demos title properties. + /// + /// In en, this message translates to: + /// **'Title'** + String get demoMotionPlaceholderTitle; + + /// The placeholder for the motion demos subtitle properties. + /// + /// In en, this message translates to: + /// **'Secondary text'** + String get demoMotionPlaceholderSubtitle; + + /// The placeholder for the motion demos shortened subtitle properties. + /// + /// In en, this message translates to: + /// **'Secondary'** + String get demoMotionSmallPlaceholderSubtitle; + + /// The title for the details page in the motion demos. + /// + /// In en, this message translates to: + /// **'Details Page'** + String get demoMotionDetailsPageTitle; + + /// The title for a list tile in the motion demos. + /// + /// In en, this message translates to: + /// **'List item'** + String get demoMotionListTileTitle; + + /// Description for the shared y axis demo. + /// + /// In en, this message translates to: + /// **'The shared axis pattern is used for transitions between the UI elements that have a spatial or navigational relationship. This pattern uses a shared transformation on the x, y, or z axis to reinforce the relationship between elements.'** + String get demoSharedAxisDescription; + + /// Title for the shared x axis demo. + /// + /// In en, this message translates to: + /// **'Shared x-axis'** + String get demoSharedXAxisTitle; + + /// Button text for back button in the shared x axis demo. + /// + /// In en, this message translates to: + /// **'BACK'** + String get demoSharedXAxisBackButtonText; + + /// Button text for the next button in the shared x axis demo. + /// + /// In en, this message translates to: + /// **'NEXT'** + String get demoSharedXAxisNextButtonText; + + /// Title for course selection page in the shared x axis demo. + /// + /// In en, this message translates to: + /// **'Streamline your courses'** + String get demoSharedXAxisCoursePageTitle; + + /// Subtitle for course selection page in the shared x axis demo. + /// + /// In en, this message translates to: + /// **'Bundled categories appear as groups in your feed. You can always change this later.'** + String get demoSharedXAxisCoursePageSubtitle; + + /// Title for the Arts & Crafts course in the shared x axis demo. + /// + /// In en, this message translates to: + /// **'Arts & Crafts'** + String get demoSharedXAxisArtsAndCraftsCourseTitle; + + /// Title for the Business course in the shared x axis demo. + /// + /// In en, this message translates to: + /// **'Business'** + String get demoSharedXAxisBusinessCourseTitle; + + /// Title for the Illustration course in the shared x axis demo. + /// + /// In en, this message translates to: + /// **'Illustration'** + String get demoSharedXAxisIllustrationCourseTitle; + + /// Title for the Design course in the shared x axis demo. + /// + /// In en, this message translates to: + /// **'Design'** + String get demoSharedXAxisDesignCourseTitle; + + /// Title for the Culinary course in the shared x axis demo. + /// + /// In en, this message translates to: + /// **'Culinary'** + String get demoSharedXAxisCulinaryCourseTitle; + + /// Subtitle for a bundled course in the shared x axis demo. + /// + /// In en, this message translates to: + /// **'Bundled'** + String get demoSharedXAxisBundledCourseSubtitle; + + /// Subtitle for a individual course in the shared x axis demo. + /// + /// In en, this message translates to: + /// **'Shown Individually'** + String get demoSharedXAxisIndividualCourseSubtitle; + + /// Welcome text for sign in page in the shared x axis demo. David Park is a name and does not need to be translated. + /// + /// In en, this message translates to: + /// **'Hi David Park'** + String get demoSharedXAxisSignInWelcomeText; + + /// Subtitle text for sign in page in the shared x axis demo. + /// + /// In en, this message translates to: + /// **'Sign in with your account'** + String get demoSharedXAxisSignInSubtitleText; + + /// Label text for the sign in text field in the shared x axis demo. + /// + /// In en, this message translates to: + /// **'Email or phone number'** + String get demoSharedXAxisSignInTextFieldLabel; + + /// Button text for the forgot email button in the shared x axis demo. + /// + /// In en, this message translates to: + /// **'FORGOT EMAIL?'** + String get demoSharedXAxisForgotEmailButtonText; + + /// Button text for the create account button in the shared x axis demo. + /// + /// In en, this message translates to: + /// **'CREATE ACCOUNT'** + String get demoSharedXAxisCreateAccountButtonText; + + /// Title for the shared y axis demo. + /// + /// In en, this message translates to: + /// **'Shared y-axis'** + String get demoSharedYAxisTitle; + + /// Text for album count in the shared y axis demo. + /// + /// In en, this message translates to: + /// **'268 albums'** + String get demoSharedYAxisAlbumCount; + + /// Title for alphabetical sorting type in the shared y axis demo. + /// + /// In en, this message translates to: + /// **'A-Z'** + String get demoSharedYAxisAlphabeticalSortTitle; + + /// Title for recently played sorting type in the shared y axis demo. + /// + /// In en, this message translates to: + /// **'Recently played'** + String get demoSharedYAxisRecentSortTitle; + + /// Title for an AlbumTile in the shared y axis demo. + /// + /// In en, this message translates to: + /// **'Album'** + String get demoSharedYAxisAlbumTileTitle; + + /// Subtitle for an AlbumTile in the shared y axis demo. + /// + /// In en, this message translates to: + /// **'Artist'** + String get demoSharedYAxisAlbumTileSubtitle; + + /// Duration unit for an AlbumTile in the shared y axis demo. + /// + /// In en, this message translates to: + /// **'min'** + String get demoSharedYAxisAlbumTileDurationUnit; + + /// Title for the shared z axis demo. + /// + /// In en, this message translates to: + /// **'Shared z-axis'** + String get demoSharedZAxisTitle; + + /// Title for the settings page in the shared z axis demo. + /// + /// In en, this message translates to: + /// **'Settings'** + String get demoSharedZAxisSettingsPageTitle; + + /// Title for burger recipe tile in the shared z axis demo. + /// + /// In en, this message translates to: + /// **'Burger'** + String get demoSharedZAxisBurgerRecipeTitle; + + /// Subtitle for the burger recipe tile in the shared z axis demo. + /// + /// In en, this message translates to: + /// **'Burger recipe'** + String get demoSharedZAxisBurgerRecipeDescription; + + /// Title for sandwich recipe tile in the shared z axis demo. + /// + /// In en, this message translates to: + /// **'Sandwich'** + String get demoSharedZAxisSandwichRecipeTitle; + + /// Subtitle for the sandwich recipe tile in the shared z axis demo. + /// + /// In en, this message translates to: + /// **'Sandwich recipe'** + String get demoSharedZAxisSandwichRecipeDescription; + + /// Title for dessert recipe tile in the shared z axis demo. + /// + /// In en, this message translates to: + /// **'Dessert'** + String get demoSharedZAxisDessertRecipeTitle; + + /// Subtitle for the dessert recipe tile in the shared z axis demo. + /// + /// In en, this message translates to: + /// **'Dessert recipe'** + String get demoSharedZAxisDessertRecipeDescription; + + /// Title for shrimp plate recipe tile in the shared z axis demo. + /// + /// In en, this message translates to: + /// **'Shrimp'** + String get demoSharedZAxisShrimpPlateRecipeTitle; + + /// Subtitle for the shrimp plate recipe tile in the shared z axis demo. + /// + /// In en, this message translates to: + /// **'Shrimp plate recipe'** + String get demoSharedZAxisShrimpPlateRecipeDescription; + + /// Title for crab plate recipe tile in the shared z axis demo. + /// + /// In en, this message translates to: + /// **'Crab'** + String get demoSharedZAxisCrabPlateRecipeTitle; + + /// Subtitle for the crab plate recipe tile in the shared z axis demo. + /// + /// In en, this message translates to: + /// **'Crab plate recipe'** + String get demoSharedZAxisCrabPlateRecipeDescription; + + /// Title for beef sandwich recipe tile in the shared z axis demo. + /// + /// In en, this message translates to: + /// **'Beef Sandwich'** + String get demoSharedZAxisBeefSandwichRecipeTitle; + + /// Subtitle for the beef sandwich recipe tile in the shared z axis demo. + /// + /// In en, this message translates to: + /// **'Beef Sandwich recipe'** + String get demoSharedZAxisBeefSandwichRecipeDescription; + + /// Title for list of saved recipes in the shared z axis demo. + /// + /// In en, this message translates to: + /// **'Saved Recipes'** + String get demoSharedZAxisSavedRecipesListTitle; + + /// Text label for profile setting tile in the shared z axis demo. + /// + /// In en, this message translates to: + /// **'Profile'** + String get demoSharedZAxisProfileSettingLabel; + + /// Text label for notifications setting tile in the shared z axis demo. + /// + /// In en, this message translates to: + /// **'Notifications'** + String get demoSharedZAxisNotificationSettingLabel; + + /// Text label for the privacy setting tile in the shared z axis demo. + /// + /// In en, this message translates to: + /// **'Privacy'** + String get demoSharedZAxisPrivacySettingLabel; + + /// Text label for the help setting tile in the shared z axis demo. + /// + /// In en, this message translates to: + /// **'Help'** + String get demoSharedZAxisHelpSettingLabel; + + /// Title for the fade through demo. + /// + /// In en, this message translates to: + /// **'Fade through'** + String get demoFadeThroughTitle; + + /// Description for the fade through demo. + /// + /// In en, this message translates to: + /// **'The fade through pattern is used for transitions between UI elements that do not have a strong relationship to each other.'** + String get demoFadeThroughDescription; + + /// Text for albums bottom navigation bar destination in the fade through demo. + /// + /// In en, this message translates to: + /// **'Albums'** + String get demoFadeThroughAlbumsDestination; + + /// Text for photos bottom navigation bar destination in the fade through demo. + /// + /// In en, this message translates to: + /// **'Photos'** + String get demoFadeThroughPhotosDestination; + + /// Text for search bottom navigation bar destination in the fade through demo. + /// + /// In en, this message translates to: + /// **'Search'** + String get demoFadeThroughSearchDestination; + + /// Placeholder for example card title in the fade through demo. + /// + /// In en, this message translates to: + /// **'123 photos'** + String get demoFadeThroughTextPlaceholder; + + /// Title for the fade scale demo. + /// + /// In en, this message translates to: + /// **'Fade'** + String get demoFadeScaleTitle; + + /// Description for the fade scale demo. + /// + /// In en, this message translates to: + /// **'The fade pattern is used for UI elements that enter or exit within the bounds of the screen, such as a dialog that fades in the center of the screen.'** + String get demoFadeScaleDescription; + + /// Button text to show alert dialog in the fade scale demo. + /// + /// In en, this message translates to: + /// **'SHOW MODAL'** + String get demoFadeScaleShowAlertDialogButton; + + /// Button text to show fab in the fade scale demo. + /// + /// In en, this message translates to: + /// **'SHOW FAB'** + String get demoFadeScaleShowFabButton; + + /// Button text to hide fab in the fade scale demo. + /// + /// In en, this message translates to: + /// **'HIDE FAB'** + String get demoFadeScaleHideFabButton; + + /// Generic header for alert dialog in the fade scale demo. + /// + /// In en, this message translates to: + /// **'Alert Dialog'** + String get demoFadeScaleAlertDialogHeader; + + /// Button text for alert dialog cancel button in the fade scale demo. + /// + /// In en, this message translates to: + /// **'CANCEL'** + String get demoFadeScaleAlertDialogCancelButton; + + /// Button text for alert dialog discard button in the fade scale demo. + /// + /// In en, this message translates to: + /// **'DISCARD'** + String get demoFadeScaleAlertDialogDiscardButton; + + /// Title for the colors demo. + /// + /// In en, this message translates to: + /// **'Colors'** + String get demoColorsTitle; + + /// Subtitle for the colors demo. + /// + /// In en, this message translates to: + /// **'All of the predefined colors'** + String get demoColorsSubtitle; + + /// Description for the colors demo. Material Design should remain capitalized. + /// + /// In en, this message translates to: + /// **'Color and color swatch constants which represent Material Design\'s color palette.'** + String get demoColorsDescription; + + /// Title for the typography demo. + /// + /// In en, this message translates to: + /// **'Typography'** + String get demoTypographyTitle; + + /// Subtitle for the typography demo. + /// + /// In en, this message translates to: + /// **'All of the predefined text styles'** + String get demoTypographySubtitle; + + /// Description for the typography demo. Material Design should remain capitalized. + /// + /// In en, this message translates to: + /// **'Definitions for the various typographical styles found in Material Design.'** + String get demoTypographyDescription; + + /// Title for the 2D transformations demo. + /// + /// In en, this message translates to: + /// **'2D transformations'** + String get demo2dTransformationsTitle; + + /// Subtitle for the 2D transformations demo. + /// + /// In en, this message translates to: + /// **'Pan and zoom'** + String get demo2dTransformationsSubtitle; + + /// Description for the 2D transformations demo. + /// + /// In en, this message translates to: + /// **'Tap to edit tiles, and use gestures to move around the scene. Drag to pan and pinch with two fingers to zoom. Press the reset button to return to the starting orientation.'** + String get demo2dTransformationsDescription; + + /// Tooltip for a button to reset the transformations (scale, translation) for the 2D transformations demo. + /// + /// In en, this message translates to: + /// **'Reset transformations'** + String get demo2dTransformationsResetTooltip; + + /// Tooltip for a button to edit a tile. + /// + /// In en, this message translates to: + /// **'Edit tile'** + String get demo2dTransformationsEditTooltip; + + /// Text for a generic button. + /// + /// In en, this message translates to: + /// **'BUTTON'** + String get buttonText; + + /// Title for bottom sheet demo. + /// + /// In en, this message translates to: + /// **'Bottom sheet'** + String get demoBottomSheetTitle; + + /// Description for bottom sheet demo. + /// + /// In en, this message translates to: + /// **'Persistent and modal bottom sheets'** + String get demoBottomSheetSubtitle; + + /// Title for persistent bottom sheet demo. + /// + /// In en, this message translates to: + /// **'Persistent bottom sheet'** + String get demoBottomSheetPersistentTitle; + + /// Description for persistent bottom sheet demo. + /// + /// In en, this message translates to: + /// **'A persistent bottom sheet shows information that supplements the primary content of the app. A persistent bottom sheet remains visible even when the user interacts with other parts of the app.'** + String get demoBottomSheetPersistentDescription; + + /// Title for modal bottom sheet demo. + /// + /// In en, this message translates to: + /// **'Modal bottom sheet'** + String get demoBottomSheetModalTitle; + + /// Description for modal bottom sheet demo. + /// + /// In en, this message translates to: + /// **'A modal bottom sheet is an alternative to a menu or a dialog and prevents the user from interacting with the rest of the app.'** + String get demoBottomSheetModalDescription; + + /// Semantic label for add icon. + /// + /// In en, this message translates to: + /// **'Add'** + String get demoBottomSheetAddLabel; + + /// Button text to show bottom sheet. + /// + /// In en, this message translates to: + /// **'SHOW BOTTOM SHEET'** + String get demoBottomSheetButtonText; + + /// Generic header placeholder. + /// + /// In en, this message translates to: + /// **'Header'** + String get demoBottomSheetHeader; + + /// Generic item placeholder. + /// + /// In en, this message translates to: + /// **'Item {value}'** + String demoBottomSheetItem(Object value); + + /// Title for lists demo. + /// + /// In en, this message translates to: + /// **'Lists'** + String get demoListsTitle; + + /// Subtitle for lists demo. + /// + /// In en, this message translates to: + /// **'Scrolling list layouts'** + String get demoListsSubtitle; + + /// Description for lists demo. This describes what a single row in a list consists of. + /// + /// In en, this message translates to: + /// **'A single fixed-height row that typically contains some text as well as a leading or trailing icon.'** + String get demoListsDescription; + + /// Title for lists demo with only one line of text per row. + /// + /// In en, this message translates to: + /// **'One Line'** + String get demoOneLineListsTitle; + + /// Title for lists demo with two lines of text per row. + /// + /// In en, this message translates to: + /// **'Two Lines'** + String get demoTwoLineListsTitle; + + /// Text that appears in the second line of a list item. + /// + /// In en, this message translates to: + /// **'Secondary text'** + String get demoListsSecondary; + + /// Title for progress indicators demo. + /// + /// In en, this message translates to: + /// **'Progress indicators'** + String get demoProgressIndicatorTitle; + + /// Subtitle for progress indicators demo. + /// + /// In en, this message translates to: + /// **'Linear, circular, indeterminate'** + String get demoProgressIndicatorSubtitle; + + /// Title for circular progress indicator demo. + /// + /// In en, this message translates to: + /// **'Circular Progress Indicator'** + String get demoCircularProgressIndicatorTitle; + + /// Description for circular progress indicator demo. + /// + /// In en, this message translates to: + /// **'A Material Design circular progress indicator, which spins to indicate that the application is busy.'** + String get demoCircularProgressIndicatorDescription; + + /// Title for linear progress indicator demo. + /// + /// In en, this message translates to: + /// **'Linear Progress Indicator'** + String get demoLinearProgressIndicatorTitle; + + /// Description for linear progress indicator demo. + /// + /// In en, this message translates to: + /// **'A Material Design linear progress indicator, also known as a progress bar.'** + String get demoLinearProgressIndicatorDescription; + + /// Title for pickers demo. + /// + /// In en, this message translates to: + /// **'Pickers'** + String get demoPickersTitle; + + /// Subtitle for pickers demo. + /// + /// In en, this message translates to: + /// **'Date and time selection'** + String get demoPickersSubtitle; + + /// Title for date picker demo. + /// + /// In en, this message translates to: + /// **'Date Picker'** + String get demoDatePickerTitle; + + /// Description for date picker demo. + /// + /// In en, this message translates to: + /// **'Shows a dialog containing a Material Design date picker.'** + String get demoDatePickerDescription; + + /// Title for time picker demo. + /// + /// In en, this message translates to: + /// **'Time Picker'** + String get demoTimePickerTitle; + + /// Description for time picker demo. + /// + /// In en, this message translates to: + /// **'Shows a dialog containing a Material Design time picker.'** + String get demoTimePickerDescription; + + /// Title for date range picker demo. + /// + /// In en, this message translates to: + /// **'Date Range Picker'** + String get demoDateRangePickerTitle; + + /// Description for date range picker demo. + /// + /// In en, this message translates to: + /// **'Shows a dialog containing a Material Design date range picker.'** + String get demoDateRangePickerDescription; + + /// Button text to show the date or time picker in the demo. + /// + /// In en, this message translates to: + /// **'SHOW PICKER'** + String get demoPickersShowPicker; + + /// Title for tabs demo. + /// + /// In en, this message translates to: + /// **'Tabs'** + String get demoTabsTitle; + + /// Title for tabs demo with a tab bar that scrolls. + /// + /// In en, this message translates to: + /// **'Scrolling'** + String get demoTabsScrollingTitle; + + /// Title for tabs demo with a tab bar that doesn't scroll. + /// + /// In en, this message translates to: + /// **'Non-scrolling'** + String get demoTabsNonScrollingTitle; + + /// Subtitle for tabs demo. + /// + /// In en, this message translates to: + /// **'Tabs with independently scrollable views'** + String get demoTabsSubtitle; + + /// Description for tabs demo. + /// + /// In en, this message translates to: + /// **'Tabs organize content across different screens, data sets, and other interactions.'** + String get demoTabsDescription; + + /// Title for snackbars demo. + /// + /// In en, this message translates to: + /// **'Snackbars'** + String get demoSnackbarsTitle; + + /// Subtitle for snackbars demo. + /// + /// In en, this message translates to: + /// **'Snackbars show messages at the bottom of the screen'** + String get demoSnackbarsSubtitle; + + /// Description for snackbars demo. + /// + /// In en, this message translates to: + /// **'Snackbars inform users of a process that an app has performed or will perform. They appear temporarily, towards the bottom of the screen. They shouldn\'t interrupt the user experience, and they don\'t require user input to disappear.'** + String get demoSnackbarsDescription; + + /// Label for button to show a snackbar. + /// + /// In en, this message translates to: + /// **'SHOW A SNACKBAR'** + String get demoSnackbarsButtonLabel; + + /// Text to show on a snackbar. + /// + /// In en, this message translates to: + /// **'This is a snackbar.'** + String get demoSnackbarsText; + + /// Label for action button text on the snackbar. + /// + /// In en, this message translates to: + /// **'ACTION'** + String get demoSnackbarsActionButtonLabel; + + /// Text that appears when you press on a snackbars' action. + /// + /// In en, this message translates to: + /// **'You pressed the snackbar action.'** + String get demoSnackbarsAction; + + /// Title for selection controls demo. + /// + /// In en, this message translates to: + /// **'Selection controls'** + String get demoSelectionControlsTitle; + + /// Subtitle for selection controls demo. + /// + /// In en, this message translates to: + /// **'Checkboxes, radio buttons, and switches'** + String get demoSelectionControlsSubtitle; + + /// Title for the checkbox (selection controls) demo. + /// + /// In en, this message translates to: + /// **'Checkbox'** + String get demoSelectionControlsCheckboxTitle; + + /// Description for the checkbox (selection controls) demo. + /// + /// In en, this message translates to: + /// **'Checkboxes allow the user to select multiple options from a set. A normal checkbox\'s value is true or false and a tristate checkbox\'s value can also be null.'** + String get demoSelectionControlsCheckboxDescription; + + /// Title for the radio button (selection controls) demo. + /// + /// In en, this message translates to: + /// **'Radio'** + String get demoSelectionControlsRadioTitle; + + /// Description for the radio button (selection controls) demo. + /// + /// In en, this message translates to: + /// **'Radio buttons allow the user to select one option from a set. Use radio buttons for exclusive selection if you think that the user needs to see all available options side-by-side.'** + String get demoSelectionControlsRadioDescription; + + /// Title for the switches (selection controls) demo. + /// + /// In en, this message translates to: + /// **'Switch'** + String get demoSelectionControlsSwitchTitle; + + /// Description for the switches (selection controls) demo. + /// + /// In en, this message translates to: + /// **'On/off switches toggle the state of a single settings option. The option that the switch controls, as well as the state it\'s in, should be made clear from the corresponding inline label.'** + String get demoSelectionControlsSwitchDescription; + + /// Title for text fields demo. + /// + /// In en, this message translates to: + /// **'Text fields'** + String get demoBottomTextFieldsTitle; + + /// Title for text fields demo. + /// + /// In en, this message translates to: + /// **'Text fields'** + String get demoTextFieldTitle; + + /// Description for text fields demo. + /// + /// In en, this message translates to: + /// **'Single line of editable text and numbers'** + String get demoTextFieldSubtitle; + + /// Description for text fields demo. + /// + /// In en, this message translates to: + /// **'Text fields allow users to enter text into a UI. They typically appear in forms and dialogs.'** + String get demoTextFieldDescription; + + /// Label for show password icon. + /// + /// In en, this message translates to: + /// **'Show password'** + String get demoTextFieldShowPasswordLabel; + + /// Label for hide password icon. + /// + /// In en, this message translates to: + /// **'Hide password'** + String get demoTextFieldHidePasswordLabel; + + /// Text that shows up on form errors. + /// + /// In en, this message translates to: + /// **'Please fix the errors in red before submitting.'** + String get demoTextFieldFormErrors; + + /// Shows up as submission error if name is not given in the form. + /// + /// In en, this message translates to: + /// **'Name is required.'** + String get demoTextFieldNameRequired; + + /// Error that shows if non-alphabetical characters are given. + /// + /// In en, this message translates to: + /// **'Please enter only alphabetical characters.'** + String get demoTextFieldOnlyAlphabeticalChars; + + /// Error that shows up if non-valid non-US phone number is given. + /// + /// In en, this message translates to: + /// **'(###) ###-#### - Enter a US phone number.'** + String get demoTextFieldEnterUSPhoneNumber; + + /// Error that shows up if password is not given. + /// + /// In en, this message translates to: + /// **'Please enter a password.'** + String get demoTextFieldEnterPassword; + + /// Error that shows up, if the re-typed password does not match the already given password. + /// + /// In en, this message translates to: + /// **'The passwords don\'t match'** + String get demoTextFieldPasswordsDoNotMatch; + + /// Placeholder for name field in form. + /// + /// In en, this message translates to: + /// **'What do people call you?'** + String get demoTextFieldWhatDoPeopleCallYou; + + /// The label for a name input field that is required (hence the star). + /// + /// In en, this message translates to: + /// **'Name*'** + String get demoTextFieldNameField; + + /// Placeholder for when entering a phone number in a form. + /// + /// In en, this message translates to: + /// **'Where can we reach you?'** + String get demoTextFieldWhereCanWeReachYou; + + /// The label for a phone number input field that is required (hence the star). + /// + /// In en, this message translates to: + /// **'Phone number*'** + String get demoTextFieldPhoneNumber; + + /// The label for an email address input field. + /// + /// In en, this message translates to: + /// **'Your email address'** + String get demoTextFieldYourEmailAddress; + + /// The label for an email address input field + /// + /// In en, this message translates to: + /// **'Email'** + String get demoTextFieldEmail; + + /// The placeholder text for biography/life story input field. + /// + /// In en, this message translates to: + /// **'Tell us about yourself (e.g., write down what you do or what hobbies you have)'** + String get demoTextFieldTellUsAboutYourself; + + /// Helper text for biography/life story input field. + /// + /// In en, this message translates to: + /// **'Keep it short, this is just a demo.'** + String get demoTextFieldKeepItShort; + + /// The label for biography/life story input field. + /// + /// In en, this message translates to: + /// **'Life story'** + String get demoTextFieldLifeStory; + + /// The label for salary input field. + /// + /// In en, this message translates to: + /// **'Salary'** + String get demoTextFieldSalary; + + /// US currency, used as suffix in input field for salary. + /// + /// In en, this message translates to: + /// **'USD'** + String get demoTextFieldUSD; + + /// Helper text for password input field. + /// + /// In en, this message translates to: + /// **'No more than 8 characters.'** + String get demoTextFieldNoMoreThan; + + /// Label for password input field, that is required (hence the star). + /// + /// In en, this message translates to: + /// **'Password*'** + String get demoTextFieldPassword; + + /// Label for repeat password input field. + /// + /// In en, this message translates to: + /// **'Re-type password*'** + String get demoTextFieldRetypePassword; + + /// The submit button text for form. + /// + /// In en, this message translates to: + /// **'SUBMIT'** + String get demoTextFieldSubmit; + + /// Text that shows up when valid phone number and name is submitted in form. + /// + /// In en, this message translates to: + /// **'{name} phone number is {phoneNumber}'** + String demoTextFieldNameHasPhoneNumber(Object name, Object phoneNumber); + + /// Helper text to indicate that * means that it is a required field. + /// + /// In en, this message translates to: + /// **'* indicates required field'** + String get demoTextFieldRequiredField; + + /// Title for tooltip demo. + /// + /// In en, this message translates to: + /// **'Tooltips'** + String get demoTooltipTitle; + + /// Subtitle for tooltip demo. + /// + /// In en, this message translates to: + /// **'Short message displayed on long press or hover'** + String get demoTooltipSubtitle; + + /// Description for tooltip demo. + /// + /// In en, this message translates to: + /// **'Tooltips provide text labels that help explain the function of a button or other user interface action. Tooltips display informative text when users hover over, focus on, or long press an element.'** + String get demoTooltipDescription; + + /// Instructions for how to trigger a tooltip in the tooltip demo. + /// + /// In en, this message translates to: + /// **'Long press or hover to display the tooltip.'** + String get demoTooltipInstructions; + + /// Title for Comments tab of bottom navigation. + /// + /// In en, this message translates to: + /// **'Comments'** + String get bottomNavigationCommentsTab; + + /// Title for Calendar tab of bottom navigation. + /// + /// In en, this message translates to: + /// **'Calendar'** + String get bottomNavigationCalendarTab; + + /// Title for Account tab of bottom navigation. + /// + /// In en, this message translates to: + /// **'Account'** + String get bottomNavigationAccountTab; + + /// Title for Alarm tab of bottom navigation. + /// + /// In en, this message translates to: + /// **'Alarm'** + String get bottomNavigationAlarmTab; + + /// Title for Camera tab of bottom navigation. + /// + /// In en, this message translates to: + /// **'Camera'** + String get bottomNavigationCameraTab; + + /// Accessibility label for the content placeholder in the bottom navigation demo + /// + /// In en, this message translates to: + /// **'Placeholder for {title} tab'** + String bottomNavigationContentPlaceholder(Object title); + + /// Tooltip text for a create button. + /// + /// In en, this message translates to: + /// **'Create'** + String get buttonTextCreate; + + /// Message displayed after an option is selected from a dialog + /// + /// In en, this message translates to: + /// **'You selected: \"{value}\"'** + String dialogSelectedOption(Object value); + + /// A chip component to turn on the lights. + /// + /// In en, this message translates to: + /// **'Turn on lights'** + String get chipTurnOnLights; + + /// A chip component to select a small size. + /// + /// In en, this message translates to: + /// **'Small'** + String get chipSmall; + + /// A chip component to select a medium size. + /// + /// In en, this message translates to: + /// **'Medium'** + String get chipMedium; + + /// A chip component to select a large size. + /// + /// In en, this message translates to: + /// **'Large'** + String get chipLarge; + + /// A chip component to filter selection by elevators. + /// + /// In en, this message translates to: + /// **'Elevator'** + String get chipElevator; + + /// A chip component to filter selection by washers. + /// + /// In en, this message translates to: + /// **'Washer'** + String get chipWasher; + + /// A chip component to filter selection by fireplaces. + /// + /// In en, this message translates to: + /// **'Fireplace'** + String get chipFireplace; + + /// A chip component to that indicates a biking selection. + /// + /// In en, this message translates to: + /// **'Biking'** + String get chipBiking; + + /// Used in the title of the demos. + /// + /// In en, this message translates to: + /// **'Demo'** + String get demo; + + /// Used as semantic label for a BottomAppBar. + /// + /// In en, this message translates to: + /// **'Bottom app bar'** + String get bottomAppBar; + + /// Indicates the loading process. + /// + /// In en, this message translates to: + /// **'Loading'** + String get loading; + + /// Alert dialog message to discard draft. + /// + /// In en, this message translates to: + /// **'Discard draft?'** + String get dialogDiscardTitle; + + /// Alert dialog title to use location services. + /// + /// In en, this message translates to: + /// **'Use Google\'s location service?'** + String get dialogLocationTitle; + + /// Alert dialog description to use location services. + /// + /// In en, this message translates to: + /// **'Let Google help apps determine location. This means sending anonymous location data to Google, even when no apps are running.'** + String get dialogLocationDescription; + + /// Alert dialog cancel option. + /// + /// In en, this message translates to: + /// **'CANCEL'** + String get dialogCancel; + + /// Alert dialog discard option. + /// + /// In en, this message translates to: + /// **'DISCARD'** + String get dialogDiscard; + + /// Alert dialog disagree option. + /// + /// In en, this message translates to: + /// **'DISAGREE'** + String get dialogDisagree; + + /// Alert dialog agree option. + /// + /// In en, this message translates to: + /// **'AGREE'** + String get dialogAgree; + + /// Alert dialog title for setting a backup account. + /// + /// In en, this message translates to: + /// **'Set backup account'** + String get dialogSetBackup; + + /// Alert dialog option for adding an account. + /// + /// In en, this message translates to: + /// **'Add account'** + String get dialogAddAccount; + + /// Button text to display a dialog. + /// + /// In en, this message translates to: + /// **'SHOW DIALOG'** + String get dialogShow; + + /// Title for full screen dialog demo. + /// + /// In en, this message translates to: + /// **'Full Screen Dialog'** + String get dialogFullscreenTitle; + + /// Save button for full screen dialog demo. + /// + /// In en, this message translates to: + /// **'SAVE'** + String get dialogFullscreenSave; + + /// Description for full screen dialog demo. + /// + /// In en, this message translates to: + /// **'A full screen dialog demo'** + String get dialogFullscreenDescription; + + /// Button text for a generic iOS-style button. + /// + /// In en, this message translates to: + /// **'Button'** + String get cupertinoButton; + + /// Button text for a iOS-style button with a filled background. + /// + /// In en, this message translates to: + /// **'With Background'** + String get cupertinoButtonWithBackground; + + /// iOS-style alert cancel option. + /// + /// In en, this message translates to: + /// **'Cancel'** + String get cupertinoAlertCancel; + + /// iOS-style alert discard option. + /// + /// In en, this message translates to: + /// **'Discard'** + String get cupertinoAlertDiscard; + + /// iOS-style alert title for location permission. + /// + /// In en, this message translates to: + /// **'Allow \"Maps\" to access your location while you are using the app?'** + String get cupertinoAlertLocationTitle; + + /// iOS-style alert description for location permission. + /// + /// In en, this message translates to: + /// **'Your current location will be displayed on the map and used for directions, nearby search results, and estimated travel times.'** + String get cupertinoAlertLocationDescription; + + /// iOS-style alert allow option. + /// + /// In en, this message translates to: + /// **'Allow'** + String get cupertinoAlertAllow; + + /// iOS-style alert don't allow option. + /// + /// In en, this message translates to: + /// **'Don\'t Allow'** + String get cupertinoAlertDontAllow; + + /// iOS-style alert title for selecting favorite dessert. + /// + /// In en, this message translates to: + /// **'Select Favorite Dessert'** + String get cupertinoAlertFavoriteDessert; + + /// iOS-style alert description for selecting favorite dessert. + /// + /// In en, this message translates to: + /// **'Please select your favorite type of dessert from the list below. Your selection will be used to customize the suggested list of eateries in your area.'** + String get cupertinoAlertDessertDescription; + + /// iOS-style alert cheesecake option. + /// + /// In en, this message translates to: + /// **'Cheesecake'** + String get cupertinoAlertCheesecake; + + /// iOS-style alert tiramisu option. + /// + /// In en, this message translates to: + /// **'Tiramisu'** + String get cupertinoAlertTiramisu; + + /// iOS-style alert apple pie option. + /// + /// In en, this message translates to: + /// **'Apple Pie'** + String get cupertinoAlertApplePie; + + /// iOS-style alert chocolate brownie option. + /// + /// In en, this message translates to: + /// **'Chocolate Brownie'** + String get cupertinoAlertChocolateBrownie; + + /// Button text to show iOS-style alert. + /// + /// In en, this message translates to: + /// **'Show Alert'** + String get cupertinoShowAlert; + + /// Tab title for the color red. + /// + /// In en, this message translates to: + /// **'RED'** + String get colorsRed; + + /// Tab title for the color pink. + /// + /// In en, this message translates to: + /// **'PINK'** + String get colorsPink; + + /// Tab title for the color purple. + /// + /// In en, this message translates to: + /// **'PURPLE'** + String get colorsPurple; + + /// Tab title for the color deep purple. + /// + /// In en, this message translates to: + /// **'DEEP PURPLE'** + String get colorsDeepPurple; + + /// Tab title for the color indigo. + /// + /// In en, this message translates to: + /// **'INDIGO'** + String get colorsIndigo; + + /// Tab title for the color blue. + /// + /// In en, this message translates to: + /// **'BLUE'** + String get colorsBlue; + + /// Tab title for the color light blue. + /// + /// In en, this message translates to: + /// **'LIGHT BLUE'** + String get colorsLightBlue; + + /// Tab title for the color cyan. + /// + /// In en, this message translates to: + /// **'CYAN'** + String get colorsCyan; + + /// Tab title for the color teal. + /// + /// In en, this message translates to: + /// **'TEAL'** + String get colorsTeal; + + /// Tab title for the color green. + /// + /// In en, this message translates to: + /// **'GREEN'** + String get colorsGreen; + + /// Tab title for the color light green. + /// + /// In en, this message translates to: + /// **'LIGHT GREEN'** + String get colorsLightGreen; + + /// Tab title for the color lime. + /// + /// In en, this message translates to: + /// **'LIME'** + String get colorsLime; + + /// Tab title for the color yellow. + /// + /// In en, this message translates to: + /// **'YELLOW'** + String get colorsYellow; + + /// Tab title for the color amber. + /// + /// In en, this message translates to: + /// **'AMBER'** + String get colorsAmber; + + /// Tab title for the color orange. + /// + /// In en, this message translates to: + /// **'ORANGE'** + String get colorsOrange; + + /// Tab title for the color deep orange. + /// + /// In en, this message translates to: + /// **'DEEP ORANGE'** + String get colorsDeepOrange; + + /// Tab title for the color brown. + /// + /// In en, this message translates to: + /// **'BROWN'** + String get colorsBrown; + + /// Tab title for the color grey. + /// + /// In en, this message translates to: + /// **'GREY'** + String get colorsGrey; + + /// Tab title for the color blue grey. + /// + /// In en, this message translates to: + /// **'BLUE GREY'** + String get colorsBlueGrey; + + /// Title for Chennai location. + /// + /// In en, this message translates to: + /// **'Chennai'** + String get placeChennai; + + /// Title for Tanjore location. + /// + /// In en, this message translates to: + /// **'Tanjore'** + String get placeTanjore; + + /// Title for Chettinad location. + /// + /// In en, this message translates to: + /// **'Chettinad'** + String get placeChettinad; + + /// Title for Pondicherry location. + /// + /// In en, this message translates to: + /// **'Pondicherry'** + String get placePondicherry; + + /// Title for Flower Market location. + /// + /// In en, this message translates to: + /// **'Flower Market'** + String get placeFlowerMarket; + + /// Title for Bronze Works location. + /// + /// In en, this message translates to: + /// **'Bronze Works'** + String get placeBronzeWorks; + + /// Title for Market location. + /// + /// In en, this message translates to: + /// **'Market'** + String get placeMarket; + + /// Title for Thanjavur Temple location. + /// + /// In en, this message translates to: + /// **'Thanjavur Temple'** + String get placeThanjavurTemple; + + /// Title for Salt Farm location. + /// + /// In en, this message translates to: + /// **'Salt Farm'** + String get placeSaltFarm; + + /// Title for image of people riding on scooters. + /// + /// In en, this message translates to: + /// **'Scooters'** + String get placeScooters; + + /// Title for an image of a silk maker. + /// + /// In en, this message translates to: + /// **'Silk Maker'** + String get placeSilkMaker; + + /// Title for an image of preparing lunch. + /// + /// In en, this message translates to: + /// **'Lunch Prep'** + String get placeLunchPrep; + + /// Title for Beach location. + /// + /// In en, this message translates to: + /// **'Beach'** + String get placeBeach; + + /// Title for an image of a fisherman. + /// + /// In en, this message translates to: + /// **'Fisherman'** + String get placeFisherman; + + /// The title and name for the starter app. + /// + /// In en, this message translates to: + /// **'Starter app'** + String get starterAppTitle; + + /// The description for the starter app. + /// + /// In en, this message translates to: + /// **'A responsive starter layout'** + String get starterAppDescription; + + /// Generic placeholder for button. + /// + /// In en, this message translates to: + /// **'BUTTON'** + String get starterAppGenericButton; + + /// Tooltip on add icon. + /// + /// In en, this message translates to: + /// **'Add'** + String get starterAppTooltipAdd; + + /// Tooltip on favorite icon. + /// + /// In en, this message translates to: + /// **'Favorite'** + String get starterAppTooltipFavorite; + + /// Tooltip on share icon. + /// + /// In en, this message translates to: + /// **'Share'** + String get starterAppTooltipShare; + + /// Tooltip on search icon. + /// + /// In en, this message translates to: + /// **'Search'** + String get starterAppTooltipSearch; + + /// Generic placeholder for title in app bar. + /// + /// In en, this message translates to: + /// **'Title'** + String get starterAppGenericTitle; + + /// Generic placeholder for subtitle in drawer. + /// + /// In en, this message translates to: + /// **'Subtitle'** + String get starterAppGenericSubtitle; + + /// Generic placeholder for headline in drawer. + /// + /// In en, this message translates to: + /// **'Headline'** + String get starterAppGenericHeadline; + + /// Generic placeholder for body text in drawer. + /// + /// In en, this message translates to: + /// **'Body'** + String get starterAppGenericBody; + + /// Generic placeholder drawer item. + /// + /// In en, this message translates to: + /// **'Item {value}'** + String starterAppDrawerItem(Object value); + + /// Caption for a menu page. + /// + /// In en, this message translates to: + /// **'MENU'** + String get shrineMenuCaption; + + /// A tab showing products from all categories. + /// + /// In en, this message translates to: + /// **'ALL'** + String get shrineCategoryNameAll; + + /// A category of products consisting of accessories (clothing items). + /// + /// In en, this message translates to: + /// **'ACCESSORIES'** + String get shrineCategoryNameAccessories; + + /// A category of products consisting of clothing. + /// + /// In en, this message translates to: + /// **'CLOTHING'** + String get shrineCategoryNameClothing; + + /// A category of products consisting of items used at home. + /// + /// In en, this message translates to: + /// **'HOME'** + String get shrineCategoryNameHome; + + /// Label for a logout button. + /// + /// In en, this message translates to: + /// **'LOGOUT'** + String get shrineLogoutButtonCaption; + + /// On the login screen, a label for a textfield for the user to input their username. + /// + /// In en, this message translates to: + /// **'Username'** + String get shrineLoginUsernameLabel; + + /// On the login screen, a label for a textfield for the user to input their password. + /// + /// In en, this message translates to: + /// **'Password'** + String get shrineLoginPasswordLabel; + + /// On the login screen, the caption for a button to cancel login. + /// + /// In en, this message translates to: + /// **'CANCEL'** + String get shrineCancelButtonCaption; + + /// On the login screen, the caption for a button to proceed login. + /// + /// In en, this message translates to: + /// **'NEXT'** + String get shrineNextButtonCaption; + + /// Caption for a shopping cart page. + /// + /// In en, this message translates to: + /// **'CART'** + String get shrineCartPageCaption; + + /// A text showing the number of items for a specific product. + /// + /// In en, this message translates to: + /// **'Quantity: {quantity}'** + String shrineProductQuantity(Object quantity); + + /// A text showing the unit price of each product. Used as: 'Quantity: 3 x $129'. The currency will be handled by the formatter. + /// + /// In en, this message translates to: + /// **'x {price}'** + String shrineProductPrice(Object price); + + /// A text showing the total number of items in the cart. + /// + /// In en, this message translates to: + /// **'{quantity, plural, =0{NO ITEMS} =1{1 ITEM} other{{quantity} ITEMS}}'** + String shrineCartItemCount(num quantity); + + /// Caption for a button used to clear the cart. + /// + /// In en, this message translates to: + /// **'CLEAR CART'** + String get shrineCartClearButtonCaption; + + /// Label for a text showing total price of the items in the cart. + /// + /// In en, this message translates to: + /// **'TOTAL'** + String get shrineCartTotalCaption; + + /// Label for a text showing the subtotal price of the items in the cart (excluding shipping and tax). + /// + /// In en, this message translates to: + /// **'Subtotal:'** + String get shrineCartSubtotalCaption; + + /// Label for a text showing the shipping cost for the items in the cart. + /// + /// In en, this message translates to: + /// **'Shipping:'** + String get shrineCartShippingCaption; + + /// Label for a text showing the tax for the items in the cart. + /// + /// In en, this message translates to: + /// **'Tax:'** + String get shrineCartTaxCaption; + + /// Name of the product 'Vagabond sack'. + /// + /// In en, this message translates to: + /// **'Vagabond sack'** + String get shrineProductVagabondSack; + + /// Name of the product 'Stella sunglasses'. + /// + /// In en, this message translates to: + /// **'Stella sunglasses'** + String get shrineProductStellaSunglasses; + + /// Name of the product 'Whitney belt'. + /// + /// In en, this message translates to: + /// **'Whitney belt'** + String get shrineProductWhitneyBelt; + + /// Name of the product 'Garden strand'. + /// + /// In en, this message translates to: + /// **'Garden strand'** + String get shrineProductGardenStrand; + + /// Name of the product 'Strut earrings'. + /// + /// In en, this message translates to: + /// **'Strut earrings'** + String get shrineProductStrutEarrings; + + /// Name of the product 'Varsity socks'. + /// + /// In en, this message translates to: + /// **'Varsity socks'** + String get shrineProductVarsitySocks; + + /// Name of the product 'Weave keyring'. + /// + /// In en, this message translates to: + /// **'Weave keyring'** + String get shrineProductWeaveKeyring; + + /// Name of the product 'Gatsby hat'. + /// + /// In en, this message translates to: + /// **'Gatsby hat'** + String get shrineProductGatsbyHat; + + /// Name of the product 'Shrug bag'. + /// + /// In en, this message translates to: + /// **'Shrug bag'** + String get shrineProductShrugBag; + + /// Name of the product 'Gilt desk trio'. + /// + /// In en, this message translates to: + /// **'Gilt desk trio'** + String get shrineProductGiltDeskTrio; + + /// Name of the product 'Copper wire rack'. + /// + /// In en, this message translates to: + /// **'Copper wire rack'** + String get shrineProductCopperWireRack; + + /// Name of the product 'Soothe ceramic set'. + /// + /// In en, this message translates to: + /// **'Soothe ceramic set'** + String get shrineProductSootheCeramicSet; + + /// Name of the product 'Hurrahs tea set'. + /// + /// In en, this message translates to: + /// **'Hurrahs tea set'** + String get shrineProductHurrahsTeaSet; + + /// Name of the product 'Blue stone mug'. + /// + /// In en, this message translates to: + /// **'Blue stone mug'** + String get shrineProductBlueStoneMug; + + /// Name of the product 'Rainwater tray'. + /// + /// In en, this message translates to: + /// **'Rainwater tray'** + String get shrineProductRainwaterTray; + + /// Name of the product 'Chambray napkins'. + /// + /// In en, this message translates to: + /// **'Chambray napkins'** + String get shrineProductChambrayNapkins; + + /// Name of the product 'Succulent planters'. + /// + /// In en, this message translates to: + /// **'Succulent planters'** + String get shrineProductSucculentPlanters; + + /// Name of the product 'Quartet table'. + /// + /// In en, this message translates to: + /// **'Quartet table'** + String get shrineProductQuartetTable; + + /// Name of the product 'Kitchen quattro'. + /// + /// In en, this message translates to: + /// **'Kitchen quattro'** + String get shrineProductKitchenQuattro; + + /// Name of the product 'Clay sweater'. + /// + /// In en, this message translates to: + /// **'Clay sweater'** + String get shrineProductClaySweater; + + /// Name of the product 'Sea tunic'. + /// + /// In en, this message translates to: + /// **'Sea tunic'** + String get shrineProductSeaTunic; + + /// Name of the product 'Plaster tunic'. + /// + /// In en, this message translates to: + /// **'Plaster tunic'** + String get shrineProductPlasterTunic; + + /// Name of the product 'White pinstripe shirt'. + /// + /// In en, this message translates to: + /// **'White pinstripe shirt'** + String get shrineProductWhitePinstripeShirt; + + /// Name of the product 'Chambray shirt'. + /// + /// In en, this message translates to: + /// **'Chambray shirt'** + String get shrineProductChambrayShirt; + + /// Name of the product 'Seabreeze sweater'. + /// + /// In en, this message translates to: + /// **'Seabreeze sweater'** + String get shrineProductSeabreezeSweater; + + /// Name of the product 'Gentry jacket'. + /// + /// In en, this message translates to: + /// **'Gentry jacket'** + String get shrineProductGentryJacket; + + /// Name of the product 'Navy trousers'. + /// + /// In en, this message translates to: + /// **'Navy trousers'** + String get shrineProductNavyTrousers; + + /// Name of the product 'Walter henley (white)'. + /// + /// In en, this message translates to: + /// **'Walter henley (white)'** + String get shrineProductWalterHenleyWhite; + + /// Name of the product 'Surf and perf shirt'. + /// + /// In en, this message translates to: + /// **'Surf and perf shirt'** + String get shrineProductSurfAndPerfShirt; + + /// Name of the product 'Ginger scarf'. + /// + /// In en, this message translates to: + /// **'Ginger scarf'** + String get shrineProductGingerScarf; + + /// Name of the product 'Ramona crossover'. + /// + /// In en, this message translates to: + /// **'Ramona crossover'** + String get shrineProductRamonaCrossover; + + /// Name of the product 'Classic white collar'. + /// + /// In en, this message translates to: + /// **'Classic white collar'** + String get shrineProductClassicWhiteCollar; + + /// Name of the product 'Cerise scallop tee'. + /// + /// In en, this message translates to: + /// **'Cerise scallop tee'** + String get shrineProductCeriseScallopTee; + + /// Name of the product 'Shoulder rolls tee'. + /// + /// In en, this message translates to: + /// **'Shoulder rolls tee'** + String get shrineProductShoulderRollsTee; + + /// Name of the product 'Grey slouch tank'. + /// + /// In en, this message translates to: + /// **'Grey slouch tank'** + String get shrineProductGreySlouchTank; + + /// Name of the product 'Sunshirt dress'. + /// + /// In en, this message translates to: + /// **'Sunshirt dress'** + String get shrineProductSunshirtDress; + + /// Name of the product 'Fine lines tee'. + /// + /// In en, this message translates to: + /// **'Fine lines tee'** + String get shrineProductFineLinesTee; + + /// The tooltip text for a search button. Also used as a semantic label, used by screen readers, such as TalkBack and VoiceOver. + /// + /// In en, this message translates to: + /// **'Search'** + String get shrineTooltipSearch; + + /// The tooltip text for a settings button. Also used as a semantic label, used by screen readers, such as TalkBack and VoiceOver. + /// + /// In en, this message translates to: + /// **'Settings'** + String get shrineTooltipSettings; + + /// The tooltip text for a menu button. Also used as a semantic label, used by screen readers, such as TalkBack and VoiceOver. + /// + /// In en, this message translates to: + /// **'Open menu'** + String get shrineTooltipOpenMenu; + + /// The tooltip text for a button to close a menu. Also used as a semantic label, used by screen readers, such as TalkBack and VoiceOver. + /// + /// In en, this message translates to: + /// **'Close menu'** + String get shrineTooltipCloseMenu; + + /// The tooltip text for a button to close the shopping cart page. Also used as a semantic label, used by screen readers, such as TalkBack and VoiceOver. + /// + /// In en, this message translates to: + /// **'Close cart'** + String get shrineTooltipCloseCart; + + /// The description of a shopping cart button containing some products. Used by screen readers, such as TalkBack and VoiceOver. + /// + /// In en, this message translates to: + /// **'{quantity, plural, =0{Shopping cart, no items} =1{Shopping cart, 1 item} other{Shopping cart, {quantity} items}}'** + String shrineScreenReaderCart(num quantity); + + /// An announcement made by screen readers, such as TalkBack and VoiceOver to indicate the action of a button for adding a product to the cart. + /// + /// In en, this message translates to: + /// **'Add to cart'** + String get shrineScreenReaderProductAddToCart; + + /// A tooltip for a button to remove a product. This will be read by screen readers, such as TalkBack and VoiceOver when a product is added to the shopping cart. + /// + /// In en, this message translates to: + /// **'Remove {product}'** + String shrineScreenReaderRemoveProductButton(Object product); + + /// The tooltip text for a button to remove an item (a product) in a shopping cart. Also used as a semantic label, used by screen readers, such as TalkBack and VoiceOver. + /// + /// In en, this message translates to: + /// **'Remove item'** + String get shrineTooltipRemoveItem; + + /// Form field label to enter the number of diners. + /// + /// In en, this message translates to: + /// **'Diners'** + String get craneFormDiners; + + /// Form field label to select a date. + /// + /// In en, this message translates to: + /// **'Select Date'** + String get craneFormDate; + + /// Form field label to select a time. + /// + /// In en, this message translates to: + /// **'Select Time'** + String get craneFormTime; + + /// Form field label to select a location. + /// + /// In en, this message translates to: + /// **'Select Location'** + String get craneFormLocation; + + /// Form field label to select the number of travellers. + /// + /// In en, this message translates to: + /// **'Travelers'** + String get craneFormTravelers; + + /// Form field label to choose a travel origin. + /// + /// In en, this message translates to: + /// **'Choose Origin'** + String get craneFormOrigin; + + /// Form field label to choose a travel destination. + /// + /// In en, this message translates to: + /// **'Choose Destination'** + String get craneFormDestination; + + /// Form field label to select multiple dates. + /// + /// In en, this message translates to: + /// **'Select Dates'** + String get craneFormDates; + + /// Generic text for an amount of hours, abbreviated to the shortest form. For example 1h. {hours} should remain untranslated. + /// + /// In en, this message translates to: + /// **'{hours, plural, =1{1h} other{{hours}h}}'** + String craneHours(num hours); + + /// Generic text for an amount of minutes, abbreviated to the shortest form. For example 15m. {minutes} should remain untranslated. + /// + /// In en, this message translates to: + /// **'{minutes, plural, =1{1m} other{{minutes}m}}'** + String craneMinutes(num minutes); + + /// A pattern to define the layout of a flight duration string. For example in English one might say 1h 15m. Translation should only rearrange the inputs. {hoursShortForm} would for example be replaced by 1h, already translated to the given locale. {minutesShortForm} would for example be replaced by 15m, already translated to the given locale. + /// + /// In en, this message translates to: + /// **'{hoursShortForm} {minutesShortForm}'** + String craneFlightDuration(Object hoursShortForm, Object minutesShortForm); + + /// Title for FLY tab. + /// + /// In en, this message translates to: + /// **'FLY'** + String get craneFly; + + /// Title for SLEEP tab. + /// + /// In en, this message translates to: + /// **'SLEEP'** + String get craneSleep; + + /// Title for EAT tab. + /// + /// In en, this message translates to: + /// **'EAT'** + String get craneEat; + + /// Subhead for FLY tab. + /// + /// In en, this message translates to: + /// **'Explore Flights by Destination'** + String get craneFlySubhead; + + /// Subhead for SLEEP tab. + /// + /// In en, this message translates to: + /// **'Explore Properties by Destination'** + String get craneSleepSubhead; + + /// Subhead for EAT tab. + /// + /// In en, this message translates to: + /// **'Explore Restaurants by Destination'** + String get craneEatSubhead; + + /// Label indicating if a flight is nonstop or how many layovers it includes. + /// + /// In en, this message translates to: + /// **'{numberOfStops, plural, =0{Nonstop} =1{1 stop} other{{numberOfStops} stops}}'** + String craneFlyStops(num numberOfStops); + + /// Text indicating the number of available properties (temporary rentals). Always plural. + /// + /// In en, this message translates to: + /// **'{totalProperties, plural, =0{No Available Properties} =1{1 Available Properties} other{{totalProperties} Available Properties}}'** + String craneSleepProperties(num totalProperties); + + /// Text indicating the number of restaurants. Always plural. + /// + /// In en, this message translates to: + /// **'{totalRestaurants, plural, =0{No Restaurants} =1{1 Restaurant} other{{totalRestaurants} Restaurants}}'** + String craneEatRestaurants(num totalRestaurants); + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Aspen, United States'** + String get craneFly0; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Big Sur, United States'** + String get craneFly1; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Khumbu Valley, Nepal'** + String get craneFly2; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Machu Picchu, Peru'** + String get craneFly3; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Malé, Maldives'** + String get craneFly4; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Vitznau, Switzerland'** + String get craneFly5; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Mexico City, Mexico'** + String get craneFly6; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Mount Rushmore, United States'** + String get craneFly7; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Singapore'** + String get craneFly8; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Havana, Cuba'** + String get craneFly9; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Cairo, Egypt'** + String get craneFly10; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Lisbon, Portugal'** + String get craneFly11; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Napa, United States'** + String get craneFly12; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Bali, Indonesia'** + String get craneFly13; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Malé, Maldives'** + String get craneSleep0; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Aspen, United States'** + String get craneSleep1; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Machu Picchu, Peru'** + String get craneSleep2; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Havana, Cuba'** + String get craneSleep3; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Vitznau, Switzerland'** + String get craneSleep4; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Big Sur, United States'** + String get craneSleep5; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Napa, United States'** + String get craneSleep6; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Porto, Portugal'** + String get craneSleep7; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Tulum, Mexico'** + String get craneSleep8; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Lisbon, Portugal'** + String get craneSleep9; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Cairo, Egypt'** + String get craneSleep10; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Taipei, Taiwan'** + String get craneSleep11; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Naples, Italy'** + String get craneEat0; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Dallas, United States'** + String get craneEat1; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Córdoba, Argentina'** + String get craneEat2; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Portland, United States'** + String get craneEat3; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Paris, France'** + String get craneEat4; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Seoul, South Korea'** + String get craneEat5; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Seattle, United States'** + String get craneEat6; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Nashville, United States'** + String get craneEat7; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Atlanta, United States'** + String get craneEat8; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Madrid, Spain'** + String get craneEat9; + + /// Label for city. + /// + /// In en, this message translates to: + /// **'Lisbon, Portugal'** + String get craneEat10; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Chalet in a snowy landscape with evergreen trees'** + String get craneFly0SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Tent in a field'** + String get craneFly1SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Prayer flags in front of snowy mountain'** + String get craneFly2SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Machu Picchu citadel'** + String get craneFly3SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Overwater bungalows'** + String get craneFly4SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Lake-side hotel in front of mountains'** + String get craneFly5SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Aerial view of Palacio de Bellas Artes'** + String get craneFly6SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Mount Rushmore'** + String get craneFly7SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Supertree Grove'** + String get craneFly8SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Man leaning on an antique blue car'** + String get craneFly9SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Al-Azhar Mosque towers during sunset'** + String get craneFly10SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Brick lighthouse at sea'** + String get craneFly11SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Pool with palm trees'** + String get craneFly12SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Sea-side pool with palm trees'** + String get craneFly13SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Overwater bungalows'** + String get craneSleep0SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Chalet in a snowy landscape with evergreen trees'** + String get craneSleep1SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Machu Picchu citadel'** + String get craneSleep2SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Man leaning on an antique blue car'** + String get craneSleep3SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Lake-side hotel in front of mountains'** + String get craneSleep4SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Tent in a field'** + String get craneSleep5SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Pool with palm trees'** + String get craneSleep6SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Colorful apartments at Riberia Square'** + String get craneSleep7SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Mayan ruins on a cliff above a beach'** + String get craneSleep8SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Brick lighthouse at sea'** + String get craneSleep9SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Al-Azhar Mosque towers during sunset'** + String get craneSleep10SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Taipei 101 skyscraper'** + String get craneSleep11SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Pizza in a wood-fired oven'** + String get craneEat0SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Empty bar with diner-style stools'** + String get craneEat1SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Burger'** + String get craneEat2SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Korean taco'** + String get craneEat3SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Chocolate dessert'** + String get craneEat4SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Artsy restaurant seating area'** + String get craneEat5SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Shrimp dish'** + String get craneEat6SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Bakery entrance'** + String get craneEat7SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Plate of crawfish'** + String get craneEat8SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Cafe counter with pastries'** + String get craneEat9SemanticLabel; + + /// Semantic label for an image. + /// + /// In en, this message translates to: + /// **'Woman holding huge pastrami sandwich'** + String get craneEat10SemanticLabel; + + /// Menu item for the front page of the news app. + /// + /// In en, this message translates to: + /// **'Front Page'** + String get fortnightlyMenuFrontPage; + + /// Menu item for the world news section of the news app. + /// + /// In en, this message translates to: + /// **'World'** + String get fortnightlyMenuWorld; + + /// Menu item for the United States news section of the news app. + /// + /// In en, this message translates to: + /// **'US'** + String get fortnightlyMenuUS; + + /// Menu item for the political news section of the news app. + /// + /// In en, this message translates to: + /// **'Politics'** + String get fortnightlyMenuPolitics; + + /// Menu item for the business news section of the news app. + /// + /// In en, this message translates to: + /// **'Business'** + String get fortnightlyMenuBusiness; + + /// Menu item for the tech news section of the news app. + /// + /// In en, this message translates to: + /// **'Tech'** + String get fortnightlyMenuTech; + + /// Menu item for the science news section of the news app. + /// + /// In en, this message translates to: + /// **'Science'** + String get fortnightlyMenuScience; + + /// Menu item for the sports news section of the news app. + /// + /// In en, this message translates to: + /// **'Sports'** + String get fortnightlyMenuSports; + + /// Menu item for the travel news section of the news app. + /// + /// In en, this message translates to: + /// **'Travel'** + String get fortnightlyMenuTravel; + + /// Menu item for the culture news section of the news app. + /// + /// In en, this message translates to: + /// **'Culture'** + String get fortnightlyMenuCulture; + + /// Hashtag for the tech design trending topic of the news app. + /// + /// In en, this message translates to: + /// **'TechDesign'** + String get fortnightlyTrendingTechDesign; + + /// Hashtag for the reform trending topic of the news app. + /// + /// In en, this message translates to: + /// **'Reform'** + String get fortnightlyTrendingReform; + + /// Hashtag for the healthcare revolution trending topic of the news app. + /// + /// In en, this message translates to: + /// **'HealthcareRevolution'** + String get fortnightlyTrendingHealthcareRevolution; + + /// Hashtag for the green army trending topic of the news app. + /// + /// In en, this message translates to: + /// **'GreenArmy'** + String get fortnightlyTrendingGreenArmy; + + /// Hashtag for the stocks trending topic of the news app. + /// + /// In en, this message translates to: + /// **'Stocks'** + String get fortnightlyTrendingStocks; + + /// Title for news section regarding the latest updates. + /// + /// In en, this message translates to: + /// **'Latest Updates'** + String get fortnightlyLatestUpdates; + + /// Headline for a news article about healthcare. + /// + /// In en, this message translates to: + /// **'The Quiet, Yet Powerful Healthcare Revolution'** + String get fortnightlyHeadlineHealthcare; + + /// Headline for a news article about war. + /// + /// In en, this message translates to: + /// **'Divided American Lives During War'** + String get fortnightlyHeadlineWar; + + /// Headline for a news article about gasoline. + /// + /// In en, this message translates to: + /// **'The Future of Gasoline'** + String get fortnightlyHeadlineGasoline; + + /// Headline for a news article about the green army. + /// + /// In en, this message translates to: + /// **'Reforming The Green Army From Within'** + String get fortnightlyHeadlineArmy; + + /// Headline for a news article about stocks. + /// + /// In en, this message translates to: + /// **'As Stocks Stagnate, Many Look To Currency'** + String get fortnightlyHeadlineStocks; + + /// Headline for a news article about fabric. + /// + /// In en, this message translates to: + /// **'Designers Use Tech To Make Futuristic Fabrics'** + String get fortnightlyHeadlineFabrics; + + /// Headline for a news article about feminists and partisanship. + /// + /// In en, this message translates to: + /// **'Feminists Take On Partisanship'** + String get fortnightlyHeadlineFeminists; + + /// Headline for a news article about bees. + /// + /// In en, this message translates to: + /// **'Farmland Bees In Short Supply'** + String get fortnightlyHeadlineBees; + + /// Text label for Inbox destination. + /// + /// In en, this message translates to: + /// **'Inbox'** + String get replyInboxLabel; + + /// Text label for Starred destination. + /// + /// In en, this message translates to: + /// **'Starred'** + String get replyStarredLabel; + + /// Text label for Sent destination. + /// + /// In en, this message translates to: + /// **'Sent'** + String get replySentLabel; + + /// Text label for Trash destination. + /// + /// In en, this message translates to: + /// **'Trash'** + String get replyTrashLabel; + + /// Text label for Spam destination. + /// + /// In en, this message translates to: + /// **'Spam'** + String get replySpamLabel; + + /// Text label for Drafts destination. + /// + /// In en, this message translates to: + /// **'Drafts'** + String get replyDraftsLabel; + + /// Option title for TwoPane demo on foldable devices. + /// + /// In en, this message translates to: + /// **'Foldable'** + String get demoTwoPaneFoldableLabel; + + /// Description for the foldable option configuration on the TwoPane demo. + /// + /// In en, this message translates to: + /// **'This is how TwoPane behaves on a foldable device.'** + String get demoTwoPaneFoldableDescription; + + /// Option title for TwoPane demo in small screen mode. Counterpart of the foldable option. + /// + /// In en, this message translates to: + /// **'Small Screen'** + String get demoTwoPaneSmallScreenLabel; + + /// Description for the small screen option configuration on the TwoPane demo. + /// + /// In en, this message translates to: + /// **'This is how TwoPane behaves on a small screen device.'** + String get demoTwoPaneSmallScreenDescription; + + /// Option title for TwoPane demo in tablet or desktop mode. + /// + /// In en, this message translates to: + /// **'Tablet / Desktop'** + String get demoTwoPaneTabletLabel; + + /// Description for the tablet / desktop option configuration on the TwoPane demo. + /// + /// In en, this message translates to: + /// **'This is how TwoPane behaves on a larger screen like a tablet or desktop.'** + String get demoTwoPaneTabletDescription; + + /// Title for the TwoPane widget demo. + /// + /// In en, this message translates to: + /// **'TwoPane'** + String get demoTwoPaneTitle; + + /// Subtitle for the TwoPane widget demo. + /// + /// In en, this message translates to: + /// **'Responsive layouts on foldable, large, and small screens'** + String get demoTwoPaneSubtitle; + + /// Tip for user, visible on the right side of the splash screen when Gallery runs on a foldable device. + /// + /// In en, this message translates to: + /// **'Select a demo'** + String get splashSelectDemo; + + /// Title of one of the panes in the TwoPane demo. It sits on top of a list of items. + /// + /// In en, this message translates to: + /// **'List'** + String get demoTwoPaneList; + + /// Title of one of the panes in the TwoPane demo, which shows details of the currently selected item. + /// + /// In en, this message translates to: + /// **'Details'** + String get demoTwoPaneDetails; + + /// Tip for user, visible on the right side of the TwoPane widget demo in the foldable configuration. + /// + /// In en, this message translates to: + /// **'Select an item'** + String get demoTwoPaneSelectItem; + + /// Generic item placeholder visible in the TwoPane widget demo. + /// + /// In en, this message translates to: + /// **'Item {value}'** + String demoTwoPaneItem(Object value); + + /// Generic item description or details visible in the TwoPane widget demo. + /// + /// In en, this message translates to: + /// **'Item {value} details'** + String demoTwoPaneItemDetails(Object value); +} + +class _GalleryLocalizationsDelegate extends LocalizationsDelegate { + const _GalleryLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupGalleryLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => ['en'].contains(locale.languageCode); + + @override + bool shouldReload(_GalleryLocalizationsDelegate old) => false; +} + +GalleryLocalizations lookupGalleryLocalizations(Locale locale) { + + // Lookup logic when language+country codes are specified. + switch (locale.languageCode) { + case 'en': { + switch (locale.countryCode) { + case 'IS': return GalleryLocalizationsEnIs(); + } + break; + } + } + + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': return GalleryLocalizationsEn(); + } + + throw FlutterError( + 'GalleryLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.' + ); +} diff --git a/dev/integration_tests/new_gallery/lib/gallery_localizations_en.dart b/dev/integration_tests/new_gallery/lib/gallery_localizations_en.dart new file mode 100644 index 0000000000..dc6ba69df6 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/gallery_localizations_en.dart @@ -0,0 +1,5080 @@ +// 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:intl/intl.dart' as intl; + +import 'gallery_localizations.dart'; + +/// The translations for English (`en`). +class GalleryLocalizationsEn extends GalleryLocalizations { + GalleryLocalizationsEn([super.locale = 'en']); + + @override + String githubRepo(Object repoName) { + return '$repoName GitHub repository'; + } + + @override + String aboutDialogDescription(Object repoLink) { + return 'To see the source code for this app, please visit the $repoLink.'; + } + + @override + String get deselect => 'Deselect'; + + @override + String get notSelected => 'Not selected'; + + @override + String get select => 'Select'; + + @override + String get selectable => 'Selectable (long press)'; + + @override + String get selected => 'Selected'; + + @override + String get signIn => 'SIGN IN'; + + @override + String get bannerDemoText => 'Your password was updated on your other device. Please sign in again.'; + + @override + String get bannerDemoResetText => 'Reset the banner'; + + @override + String get bannerDemoMultipleText => 'Multiple actions'; + + @override + String get bannerDemoLeadingText => 'Leading Icon'; + + @override + String get dismiss => 'DISMISS'; + + @override + String get backToGallery => 'Back to Gallery'; + + @override + String get cardsDemoExplore => 'Explore'; + + @override + String cardsDemoExploreSemantics(Object destinationName) { + return 'Explore $destinationName'; + } + + @override + String cardsDemoShareSemantics(Object destinationName) { + return 'Share $destinationName'; + } + + @override + String get cardsDemoTappable => 'Tappable'; + + @override + String get cardsDemoTravelDestinationTitle1 => 'Top 10 Cities to Visit in Tamil Nadu'; + + @override + String get cardsDemoTravelDestinationDescription1 => 'Number 10'; + + @override + String get cardsDemoTravelDestinationCity1 => 'Thanjavur'; + + @override + String get cardsDemoTravelDestinationLocation1 => 'Thanjavur, Tamil Nadu'; + + @override + String get cardsDemoTravelDestinationTitle2 => 'Artisans of Southern India'; + + @override + String get cardsDemoTravelDestinationDescription2 => 'Silk Spinners'; + + @override + String get cardsDemoTravelDestinationCity2 => 'Chettinad'; + + @override + String get cardsDemoTravelDestinationLocation2 => 'Sivaganga, Tamil Nadu'; + + @override + String get cardsDemoTravelDestinationTitle3 => 'Brihadisvara Temple'; + + @override + String get cardsDemoTravelDestinationDescription3 => 'Temples'; + + @override + String get homeHeaderGallery => 'Gallery'; + + @override + String get homeHeaderCategories => 'Categories'; + + @override + String get shrineDescription => 'A fashionable retail app'; + + @override + String get fortnightlyDescription => 'A content-focused news app'; + + @override + String get rallyDescription => 'A personal finance app'; + + @override + String get replyDescription => 'An efficient, focused email app'; + + @override + String get rallyAccountDataChecking => 'Checking'; + + @override + String get rallyAccountDataHomeSavings => 'Home Savings'; + + @override + String get rallyAccountDataCarSavings => 'Car Savings'; + + @override + String get rallyAccountDataVacation => 'Vacation'; + + @override + String get rallyAccountDetailDataAnnualPercentageYield => 'Annual Percentage Yield'; + + @override + String get rallyAccountDetailDataInterestRate => 'Interest Rate'; + + @override + String get rallyAccountDetailDataInterestYtd => 'Interest YTD'; + + @override + String get rallyAccountDetailDataInterestPaidLastYear => 'Interest Paid Last Year'; + + @override + String get rallyAccountDetailDataNextStatement => 'Next Statement'; + + @override + String get rallyAccountDetailDataAccountOwner => 'Account Owner'; + + @override + String get rallyBillDetailTotalAmount => 'Total Amount'; + + @override + String get rallyBillDetailAmountPaid => 'Amount Paid'; + + @override + String get rallyBillDetailAmountDue => 'Amount Due'; + + @override + String get rallyBudgetCategoryCoffeeShops => 'Coffee Shops'; + + @override + String get rallyBudgetCategoryGroceries => 'Groceries'; + + @override + String get rallyBudgetCategoryRestaurants => 'Restaurants'; + + @override + String get rallyBudgetCategoryClothing => 'Clothing'; + + @override + String get rallyBudgetDetailTotalCap => 'Total Cap'; + + @override + String get rallyBudgetDetailAmountUsed => 'Amount Used'; + + @override + String get rallyBudgetDetailAmountLeft => 'Amount Left'; + + @override + String get rallySettingsManageAccounts => 'Manage Accounts'; + + @override + String get rallySettingsTaxDocuments => 'Tax Documents'; + + @override + String get rallySettingsPasscodeAndTouchId => 'Passcode and Touch ID'; + + @override + String get rallySettingsNotifications => 'Notifications'; + + @override + String get rallySettingsPersonalInformation => 'Personal Information'; + + @override + String get rallySettingsPaperlessSettings => 'Paperless Settings'; + + @override + String get rallySettingsFindAtms => 'Find ATMs'; + + @override + String get rallySettingsHelp => 'Help'; + + @override + String get rallySettingsSignOut => 'Sign out'; + + @override + String get rallyAccountTotal => 'Total'; + + @override + String get rallyBillsDue => 'Due'; + + @override + String get rallyBudgetLeft => 'Left'; + + @override + String get rallyAccounts => 'Accounts'; + + @override + String get rallyBills => 'Bills'; + + @override + String get rallyBudgets => 'Budgets'; + + @override + String get rallyAlerts => 'Alerts'; + + @override + String get rallySeeAll => 'SEE ALL'; + + @override + String get rallyFinanceLeft => ' LEFT'; + + @override + String get rallyTitleOverview => 'OVERVIEW'; + + @override + String get rallyTitleAccounts => 'ACCOUNTS'; + + @override + String get rallyTitleBills => 'BILLS'; + + @override + String get rallyTitleBudgets => 'BUDGETS'; + + @override + String get rallyTitleSettings => 'SETTINGS'; + + @override + String get rallyLoginLoginToRally => 'Login to Rally'; + + @override + String get rallyLoginNoAccount => "Don't have an account?"; + + @override + String get rallyLoginSignUp => 'SIGN UP'; + + @override + String get rallyLoginUsername => 'Username'; + + @override + String get rallyLoginPassword => 'Password'; + + @override + String get rallyLoginLabelLogin => 'Login'; + + @override + String get rallyLoginRememberMe => 'Remember Me'; + + @override + String get rallyLoginButtonLogin => 'LOGIN'; + + @override + String rallyAlertsMessageHeadsUpShopping(Object percent) { + return "Heads up, you've used up $percent of your Shopping budget for this month."; + } + + @override + String rallyAlertsMessageSpentOnRestaurants(Object amount) { + return "You've spent $amount on Restaurants this week."; + } + + @override + String rallyAlertsMessageATMFees(Object amount) { + return "You've spent $amount in ATM fees this month"; + } + + @override + String rallyAlertsMessageCheckingAccount(Object percent) { + return 'Good work! Your checking account is $percent higher than last month.'; + } + + @override + String rallyAlertsMessageUnassignedTransactions(num count) { + final String temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Increase your potential tax deduction! Assign categories to $count unassigned transactions.', + one: 'Increase your potential tax deduction! Assign categories to 1 unassigned transaction.', + ); + return temp0; + } + + @override + String get rallySeeAllAccounts => 'See all accounts'; + + @override + String get rallySeeAllBills => 'See all bills'; + + @override + String get rallySeeAllBudgets => 'See all budgets'; + + @override + String rallyAccountAmount(Object accountName, Object accountNumber, Object amount) { + return '$accountName account $accountNumber with $amount.'; + } + + @override + String rallyBillAmount(Object billName, Object date, Object amount) { + return '$billName bill due $date for $amount.'; + } + + @override + String rallyBudgetAmount(Object budgetName, Object amountUsed, Object amountTotal, Object amountLeft) { + return '$budgetName budget with $amountUsed used of $amountTotal, $amountLeft left'; + } + + @override + String get craneDescription => 'A personalized travel app'; + + @override + String get homeCategoryReference => 'STYLES & OTHER'; + + @override + String get demoInvalidURL => "Couldn't display URL:"; + + @override + String get demoOptionsTooltip => 'Options'; + + @override + String get demoInfoTooltip => 'Info'; + + @override + String get demoCodeTooltip => 'Demo Code'; + + @override + String get demoDocumentationTooltip => 'API Documentation'; + + @override + String get demoFullscreenTooltip => 'Full Screen'; + + @override + String get demoCodeViewerCopyAll => 'COPY ALL'; + + @override + String get demoCodeViewerCopiedToClipboardMessage => 'Copied to clipboard.'; + + @override + String demoCodeViewerFailedToCopyToClipboardMessage(Object error) { + return 'Failed to copy to clipboard: $error'; + } + + @override + String get demoOptionsFeatureTitle => 'View options'; + + @override + String get demoOptionsFeatureDescription => 'Tap here to view available options for this demo.'; + + @override + String get settingsTitle => 'Settings'; + + @override + String get settingsButtonLabel => 'Settings'; + + @override + String get settingsButtonCloseLabel => 'Close settings'; + + @override + String get settingsSystemDefault => 'System'; + + @override + String get settingsTextScaling => 'Text scaling'; + + @override + String get settingsTextScalingSmall => 'Small'; + + @override + String get settingsTextScalingNormal => 'Normal'; + + @override + String get settingsTextScalingLarge => 'Large'; + + @override + String get settingsTextScalingHuge => 'Huge'; + + @override + String get settingsTextDirection => 'Text direction'; + + @override + String get settingsTextDirectionLocaleBased => 'Based on locale'; + + @override + String get settingsTextDirectionLTR => 'LTR'; + + @override + String get settingsTextDirectionRTL => 'RTL'; + + @override + String get settingsLocale => 'Locale'; + + @override + String get settingsPlatformMechanics => 'Platform mechanics'; + + @override + String get settingsTheme => 'Theme'; + + @override + String get settingsDarkTheme => 'Dark'; + + @override + String get settingsLightTheme => 'Light'; + + @override + String get settingsSlowMotion => 'Slow motion'; + + @override + String get settingsAbout => 'About Flutter Gallery'; + + @override + String get settingsFeedback => 'Send feedback'; + + @override + String get settingsAttribution => 'Designed by TOASTER in London'; + + @override + String get demoAppBarTitle => 'App bar'; + + @override + String get demoAppBarSubtitle => 'Displays information and actions relating to the current screen'; + + @override + String get demoAppBarDescription => "The App bar provides content and actions related to the current screen. It's used for branding, screen titles, navigation, and actions"; + + @override + String get demoBottomAppBarTitle => 'Bottom app bar'; + + @override + String get demoBottomAppBarSubtitle => 'Displays navigation and actions at the bottom'; + + @override + String get demoBottomAppBarDescription => 'Bottom app bars provide access to a bottom navigation drawer and up to four actions, including the floating action button.'; + + @override + String get bottomAppBarNotch => 'Notch'; + + @override + String get bottomAppBarPosition => 'Floating Action Button Position'; + + @override + String get bottomAppBarPositionDockedEnd => 'Docked - End'; + + @override + String get bottomAppBarPositionDockedCenter => 'Docked - Center'; + + @override + String get bottomAppBarPositionFloatingEnd => 'Floating - End'; + + @override + String get bottomAppBarPositionFloatingCenter => 'Floating - Center'; + + @override + String get demoBannerTitle => 'Banner'; + + @override + String get demoBannerSubtitle => 'Displaying a banner within a list'; + + @override + String get demoBannerDescription => 'A banner displays an important, succinct message, and provides actions for users to address (or dismiss the banner). A user action is required for it to be dismissed.'; + + @override + String get demoBottomNavigationTitle => 'Bottom navigation'; + + @override + String get demoBottomNavigationSubtitle => 'Bottom navigation with cross-fading views'; + + @override + String get demoBottomNavigationPersistentLabels => 'Persistent labels'; + + @override + String get demoBottomNavigationSelectedLabel => 'Selected label'; + + @override + String get demoBottomNavigationDescription => 'Bottom navigation bars display three to five destinations at the bottom of a screen. Each destination is represented by an icon and an optional text label. When a bottom navigation icon is tapped, the user is taken to the top-level navigation destination associated with that icon.'; + + @override + String get demoButtonTitle => 'Buttons'; + + @override + String get demoButtonSubtitle => 'Text, elevated, outlined, and more'; + + @override + String get demoTextButtonTitle => 'Text Button'; + + @override + String get demoTextButtonDescription => 'A text button displays an ink splash on press but does not lift. Use text buttons on toolbars, in dialogs and inline with padding'; + + @override + String get demoElevatedButtonTitle => 'Elevated Button'; + + @override + String get demoElevatedButtonDescription => 'Elevated buttons add dimension to mostly flat layouts. They emphasize functions on busy or wide spaces.'; + + @override + String get demoOutlinedButtonTitle => 'Outlined Button'; + + @override + String get demoOutlinedButtonDescription => 'Outlined buttons become opaque and elevate when pressed. They are often paired with raised buttons to indicate an alternative, secondary action.'; + + @override + String get demoToggleButtonTitle => 'Toggle Buttons'; + + @override + String get demoToggleButtonDescription => 'Toggle buttons can be used to group related options. To emphasize groups of related toggle buttons, a group should share a common container'; + + @override + String get demoFloatingButtonTitle => 'Floating Action Button'; + + @override + String get demoFloatingButtonDescription => 'A floating action button is a circular icon button that hovers over content to promote a primary action in the application.'; + + @override + String get demoCardTitle => 'Cards'; + + @override + String get demoCardSubtitle => 'Baseline cards with rounded corners'; + + @override + String get demoChipTitle => 'Chips'; + + @override + String get demoCardDescription => 'A card is a sheet of Material used to represent some related information, for example an album, a geographical location, a meal, contact details, etc.'; + + @override + String get demoChipSubtitle => 'Compact elements that represent an input, attribute, or action'; + + @override + String get demoActionChipTitle => 'Action Chip'; + + @override + String get demoActionChipDescription => 'Action chips are a set of options which trigger an action related to primary content. Action chips should appear dynamically and contextually in a UI.'; + + @override + String get demoChoiceChipTitle => 'Choice Chip'; + + @override + String get demoChoiceChipDescription => 'Choice chips represent a single choice from a set. Choice chips contain related descriptive text or categories.'; + + @override + String get demoFilterChipTitle => 'Filter Chip'; + + @override + String get demoFilterChipDescription => 'Filter chips use tags or descriptive words as a way to filter content.'; + + @override + String get demoInputChipTitle => 'Input Chip'; + + @override + String get demoInputChipDescription => 'Input chips represent a complex piece of information, such as an entity (person, place, or thing) or conversational text, in a compact form.'; + + @override + String get demoDataTableTitle => 'Data Tables'; + + @override + String get demoDataTableSubtitle => 'Rows and columns of information'; + + @override + String get demoDataTableDescription => "Data tables display information in a grid-like format of rows and columns. They organize information in a way that's easy to scan, so that users can look for patterns and insights."; + + @override + String get dataTableHeader => 'Nutrition'; + + @override + String get dataTableColumnDessert => 'Dessert (1 serving)'; + + @override + String get dataTableColumnCalories => 'Calories'; + + @override + String get dataTableColumnFat => 'Fat (g)'; + + @override + String get dataTableColumnCarbs => 'Carbs (g)'; + + @override + String get dataTableColumnProtein => 'Protein (g)'; + + @override + String get dataTableColumnSodium => 'Sodium (mg)'; + + @override + String get dataTableColumnCalcium => 'Calcium (%)'; + + @override + String get dataTableColumnIron => 'Iron (%)'; + + @override + String get dataTableRowFrozenYogurt => 'Frozen yogurt'; + + @override + String get dataTableRowIceCreamSandwich => 'Ice cream sandwich'; + + @override + String get dataTableRowEclair => 'Eclair'; + + @override + String get dataTableRowCupcake => 'Cupcake'; + + @override + String get dataTableRowGingerbread => 'Gingerbread'; + + @override + String get dataTableRowJellyBean => 'Jelly bean'; + + @override + String get dataTableRowLollipop => 'Lollipop'; + + @override + String get dataTableRowHoneycomb => 'Honeycomb'; + + @override + String get dataTableRowDonut => 'Donut'; + + @override + String get dataTableRowApplePie => 'Apple pie'; + + @override + String dataTableRowWithSugar(Object value) { + return '$value with sugar'; + } + + @override + String dataTableRowWithHoney(Object value) { + return '$value with honey'; + } + + @override + String get demoDialogTitle => 'Dialogs'; + + @override + String get demoDialogSubtitle => 'Simple, alert, and fullscreen'; + + @override + String get demoAlertDialogTitle => 'Alert'; + + @override + String get demoAlertDialogDescription => 'An alert dialog informs the user about situations that require acknowledgement. An alert dialog has an optional title and an optional list of actions.'; + + @override + String get demoAlertTitleDialogTitle => 'Alert With Title'; + + @override + String get demoSimpleDialogTitle => 'Simple'; + + @override + String get demoSimpleDialogDescription => 'A simple dialog offers the user a choice between several options. A simple dialog has an optional title that is displayed above the choices.'; + + @override + String get demoDividerTitle => 'Divider'; + + @override + String get demoDividerSubtitle => 'A divider is a thin line that groups content in lists and layouts.'; + + @override + String get demoDividerDescription => 'Dividers can be used in lists, drawers, and elsewhere to separate content.'; + + @override + String get demoVerticalDividerTitle => 'Vertical Divider'; + + @override + String get demoGridListsTitle => 'Grid Lists'; + + @override + String get demoGridListsSubtitle => 'Row and column layout'; + + @override + String get demoGridListsDescription => 'Grid Lists are best suited for presenting homogeneous data, typically images. Each item in a grid list is called a tile.'; + + @override + String get demoGridListsImageOnlyTitle => 'Image only'; + + @override + String get demoGridListsHeaderTitle => 'With header'; + + @override + String get demoGridListsFooterTitle => 'With footer'; + + @override + String get demoSlidersTitle => 'Sliders'; + + @override + String get demoSlidersSubtitle => 'Widgets for selecting a value by swiping'; + + @override + String get demoSlidersDescription => 'Sliders reflect a range of values along a bar, from which users may select a single value. They are ideal for adjusting settings such as volume, brightness, or applying image filters.'; + + @override + String get demoRangeSlidersTitle => 'Range Sliders'; + + @override + String get demoRangeSlidersDescription => 'Sliders reflect a range of values along a bar. They can have icons on both ends of the bar that reflect a range of values. They are ideal for adjusting settings such as volume, brightness, or applying image filters.'; + + @override + String get demoCustomSlidersTitle => 'Custom Sliders'; + + @override + String get demoCustomSlidersDescription => 'Sliders reflect a range of values along a bar, from which users may select a single value or range of values. The sliders can be themed and customized.'; + + @override + String get demoSlidersContinuousWithEditableNumericalValue => 'Continuous with Editable Numerical Value'; + + @override + String get demoSlidersDiscrete => 'Discrete'; + + @override + String get demoSlidersDiscreteSliderWithCustomTheme => 'Discrete Slider with Custom Theme'; + + @override + String get demoSlidersContinuousRangeSliderWithCustomTheme => 'Continuous Range Slider with Custom Theme'; + + @override + String get demoSlidersContinuous => 'Continuous'; + + @override + String get demoSlidersEditableNumericalValue => 'Editable numerical value'; + + @override + String get demoMenuTitle => 'Menu'; + + @override + String get demoContextMenuTitle => 'Context menu'; + + @override + String get demoSectionedMenuTitle => 'Sectioned menu'; + + @override + String get demoSimpleMenuTitle => 'Simple menu'; + + @override + String get demoChecklistMenuTitle => 'Checklist menu'; + + @override + String get demoMenuSubtitle => 'Menu buttons and simple menus'; + + @override + String get demoMenuDescription => 'A menu displays a list of choices on a temporary surface. They appear when users interact with a button, action, or other control.'; + + @override + String get demoMenuItemValueOne => 'Menu item one'; + + @override + String get demoMenuItemValueTwo => 'Menu item two'; + + @override + String get demoMenuItemValueThree => 'Menu item three'; + + @override + String get demoMenuOne => 'One'; + + @override + String get demoMenuTwo => 'Two'; + + @override + String get demoMenuThree => 'Three'; + + @override + String get demoMenuFour => 'Four'; + + @override + String get demoMenuAnItemWithAContextMenuButton => 'An item with a context menu'; + + @override + String get demoMenuContextMenuItemOne => 'Context menu item one'; + + @override + String get demoMenuADisabledMenuItem => 'Disabled menu item'; + + @override + String get demoMenuContextMenuItemThree => 'Context menu item three'; + + @override + String get demoMenuAnItemWithASectionedMenu => 'An item with a sectioned menu'; + + @override + String get demoMenuPreview => 'Preview'; + + @override + String get demoMenuShare => 'Share'; + + @override + String get demoMenuGetLink => 'Get link'; + + @override + String get demoMenuRemove => 'Remove'; + + @override + String demoMenuSelected(Object value) { + return 'Selected: $value'; + } + + @override + String demoMenuChecked(Object value) { + return 'Checked: $value'; + } + + @override + String get demoNavigationDrawerTitle => 'Navigation Drawer'; + + @override + String get demoNavigationDrawerSubtitle => 'Displaying a drawer within appbar'; + + @override + String get demoNavigationDrawerDescription => 'A Material Design panel that slides in horizontally from the edge of the screen to show navigation links in an application.'; + + @override + String get demoNavigationDrawerUserName => 'User Name'; + + @override + String get demoNavigationDrawerUserEmail => 'user.name@example.com'; + + @override + String get demoNavigationDrawerToPageOne => 'Item One'; + + @override + String get demoNavigationDrawerToPageTwo => 'Item Two'; + + @override + String get demoNavigationDrawerText => 'Swipe from the edge or tap the upper-left icon to see the drawer'; + + @override + String get demoNavigationRailTitle => 'Navigation Rail'; + + @override + String get demoNavigationRailSubtitle => 'Displaying a Navigation Rail within an app'; + + @override + String get demoNavigationRailDescription => 'A material widget that is meant to be displayed at the left or right of an app to navigate between a small number of views, typically between three and five.'; + + @override + String get demoNavigationRailFirst => 'First'; + + @override + String get demoNavigationRailSecond => 'Second'; + + @override + String get demoNavigationRailThird => 'Third'; + + @override + String get demoMenuAnItemWithASimpleMenu => 'An item with a simple menu'; + + @override + String get demoMenuAnItemWithAChecklistMenu => 'An item with a checklist menu'; + + @override + String get demoFullscreenDialogTitle => 'Fullscreen'; + + @override + String get demoFullscreenDialogDescription => 'The fullscreenDialog property specifies whether the incoming page is a fullscreen modal dialog'; + + @override + String get demoCupertinoActivityIndicatorTitle => 'Activity indicator'; + + @override + String get demoCupertinoActivityIndicatorSubtitle => 'iOS-style activity indicators'; + + @override + String get demoCupertinoActivityIndicatorDescription => 'An iOS-style activity indicator that spins clockwise.'; + + @override + String get demoCupertinoButtonsTitle => 'Buttons'; + + @override + String get demoCupertinoButtonsSubtitle => 'iOS-style buttons'; + + @override + String get demoCupertinoButtonsDescription => 'An iOS-style button. It takes in text and/or an icon that fades out and in on touch. May optionally have a background.'; + + @override + String get demoCupertinoContextMenuTitle => 'Context Menu'; + + @override + String get demoCupertinoContextMenuSubtitle => 'iOS-style context menu'; + + @override + String get demoCupertinoContextMenuDescription => 'An iOS-style full screen contextual menu that appears when an element is long-pressed.'; + + @override + String get demoCupertinoContextMenuActionOne => 'Action one'; + + @override + String get demoCupertinoContextMenuActionTwo => 'Action two'; + + @override + String get demoCupertinoContextMenuActionText => 'Tap and hold the Flutter logo to see the context menu.'; + + @override + String get demoCupertinoAlertsTitle => 'Alerts'; + + @override + String get demoCupertinoAlertsSubtitle => 'iOS-style alert dialogs'; + + @override + String get demoCupertinoAlertTitle => 'Alert'; + + @override + String get demoCupertinoAlertDescription => 'An alert dialog informs the user about situations that require acknowledgement. An alert dialog has an optional title, optional content, and an optional list of actions. The title is displayed above the content and the actions are displayed below the content.'; + + @override + String get demoCupertinoAlertWithTitleTitle => 'Alert With Title'; + + @override + String get demoCupertinoAlertButtonsTitle => 'Alert With Buttons'; + + @override + String get demoCupertinoAlertButtonsOnlyTitle => 'Alert Buttons Only'; + + @override + String get demoCupertinoActionSheetTitle => 'Action Sheet'; + + @override + String get demoCupertinoActionSheetDescription => 'An action sheet is a specific style of alert that presents the user with a set of two or more choices related to the current context. An action sheet can have a title, an additional message, and a list of actions.'; + + @override + String get demoCupertinoNavigationBarTitle => 'Navigation bar'; + + @override + String get demoCupertinoNavigationBarSubtitle => 'iOS-style navigation bar'; + + @override + String get demoCupertinoNavigationBarDescription => 'An iOS-styled navigation bar. The navigation bar is a toolbar that minimally consists of a page title, in the middle of the toolbar.'; + + @override + String get demoCupertinoPickerTitle => 'Pickers'; + + @override + String get demoCupertinoPickerSubtitle => 'iOS-style pickers'; + + @override + String get demoCupertinoPickerDescription => 'An iOS-style picker widget that can be used to select strings, dates, times, or both date and time.'; + + @override + String get demoCupertinoPickerTimer => 'Timer'; + + @override + String get demoCupertinoPicker => 'Picker'; + + @override + String get demoCupertinoPickerDate => 'Date'; + + @override + String get demoCupertinoPickerTime => 'Time'; + + @override + String get demoCupertinoPickerDateTime => 'Date and Time'; + + @override + String get demoCupertinoPullToRefreshTitle => 'Pull to refresh'; + + @override + String get demoCupertinoPullToRefreshSubtitle => 'iOS-style pull to refresh control'; + + @override + String get demoCupertinoPullToRefreshDescription => 'A widget implementing the iOS-style pull to refresh content control.'; + + @override + String get demoCupertinoSegmentedControlTitle => 'Segmented control'; + + @override + String get demoCupertinoSegmentedControlSubtitle => 'iOS-style segmented control'; + + @override + String get demoCupertinoSegmentedControlDescription => 'Used to select between a number of mutually exclusive options. When one option in the segmented control is selected, the other options in the segmented control cease to be selected.'; + + @override + String get demoCupertinoSliderTitle => 'Slider'; + + @override + String get demoCupertinoSliderSubtitle => 'iOS-style slider'; + + @override + String get demoCupertinoSliderDescription => 'A slider can be used to select from either a continuous or a discrete set of values.'; + + @override + String demoCupertinoSliderContinuous(Object value) { + return 'Continuous: $value'; + } + + @override + String demoCupertinoSliderDiscrete(Object value) { + return 'Discrete: $value'; + } + + @override + String get demoCupertinoSwitchSubtitle => 'iOS-style switch'; + + @override + String get demoCupertinoSwitchDescription => 'A switch is used to toggle the on/off state of a single setting.'; + + @override + String get demoCupertinoTabBarTitle => 'Tab bar'; + + @override + String get demoCupertinoTabBarSubtitle => 'iOS-style bottom tab bar'; + + @override + String get demoCupertinoTabBarDescription => 'An iOS-style bottom navigation tab bar. Displays multiple tabs with one tab being active, the first tab by default.'; + + @override + String get cupertinoTabBarHomeTab => 'Home'; + + @override + String get cupertinoTabBarChatTab => 'Chat'; + + @override + String get cupertinoTabBarProfileTab => 'Profile'; + + @override + String get demoCupertinoTextFieldTitle => 'Text fields'; + + @override + String get demoCupertinoTextFieldSubtitle => 'iOS-style text fields'; + + @override + String get demoCupertinoTextFieldDescription => 'A text field lets the user enter text, either with a hardware keyboard or with an onscreen keyboard.'; + + @override + String get demoCupertinoTextFieldPIN => 'PIN'; + + @override + String get demoCupertinoSearchTextFieldTitle => 'Search text field'; + + @override + String get demoCupertinoSearchTextFieldSubtitle => 'iOS-style search text field'; + + @override + String get demoCupertinoSearchTextFieldDescription => 'A search text field that lets the user search by entering text, and that can offer and filter suggestions.'; + + @override + String get demoCupertinoSearchTextFieldPlaceholder => 'Enter some text'; + + @override + String get demoCupertinoScrollbarTitle => 'Scrollbar'; + + @override + String get demoCupertinoScrollbarSubtitle => 'iOS-style scrollbar'; + + @override + String get demoCupertinoScrollbarDescription => 'A scrollbar that wraps the given child'; + + @override + String get demoMotionTitle => 'Motion'; + + @override + String get demoMotionSubtitle => 'All of the predefined transition patterns'; + + @override + String get demoContainerTransformDemoInstructions => 'Cards, Lists & FAB'; + + @override + String get demoSharedXAxisDemoInstructions => 'Next and Back Buttons'; + + @override + String get demoSharedYAxisDemoInstructions => 'Sort by "Recently Played"'; + + @override + String get demoSharedZAxisDemoInstructions => 'Settings icon button'; + + @override + String get demoFadeThroughDemoInstructions => 'Bottom navigation'; + + @override + String get demoFadeScaleDemoInstructions => 'Modal and FAB'; + + @override + String get demoContainerTransformTitle => 'Container Transform'; + + @override + String get demoContainerTransformDescription => 'The container transform pattern is designed for transitions between UI elements that include a container. This pattern creates a visible connection between two UI elements'; + + @override + String get demoContainerTransformModalBottomSheetTitle => 'Fade mode'; + + @override + String get demoContainerTransformTypeFade => 'FADE'; + + @override + String get demoContainerTransformTypeFadeThrough => 'FADE THROUGH'; + + @override + String get demoMotionPlaceholderTitle => 'Title'; + + @override + String get demoMotionPlaceholderSubtitle => 'Secondary text'; + + @override + String get demoMotionSmallPlaceholderSubtitle => 'Secondary'; + + @override + String get demoMotionDetailsPageTitle => 'Details Page'; + + @override + String get demoMotionListTileTitle => 'List item'; + + @override + String get demoSharedAxisDescription => 'The shared axis pattern is used for transitions between the UI elements that have a spatial or navigational relationship. This pattern uses a shared transformation on the x, y, or z axis to reinforce the relationship between elements.'; + + @override + String get demoSharedXAxisTitle => 'Shared x-axis'; + + @override + String get demoSharedXAxisBackButtonText => 'BACK'; + + @override + String get demoSharedXAxisNextButtonText => 'NEXT'; + + @override + String get demoSharedXAxisCoursePageTitle => 'Streamline your courses'; + + @override + String get demoSharedXAxisCoursePageSubtitle => 'Bundled categories appear as groups in your feed. You can always change this later.'; + + @override + String get demoSharedXAxisArtsAndCraftsCourseTitle => 'Arts & Crafts'; + + @override + String get demoSharedXAxisBusinessCourseTitle => 'Business'; + + @override + String get demoSharedXAxisIllustrationCourseTitle => 'Illustration'; + + @override + String get demoSharedXAxisDesignCourseTitle => 'Design'; + + @override + String get demoSharedXAxisCulinaryCourseTitle => 'Culinary'; + + @override + String get demoSharedXAxisBundledCourseSubtitle => 'Bundled'; + + @override + String get demoSharedXAxisIndividualCourseSubtitle => 'Shown Individually'; + + @override + String get demoSharedXAxisSignInWelcomeText => 'Hi David Park'; + + @override + String get demoSharedXAxisSignInSubtitleText => 'Sign in with your account'; + + @override + String get demoSharedXAxisSignInTextFieldLabel => 'Email or phone number'; + + @override + String get demoSharedXAxisForgotEmailButtonText => 'FORGOT EMAIL?'; + + @override + String get demoSharedXAxisCreateAccountButtonText => 'CREATE ACCOUNT'; + + @override + String get demoSharedYAxisTitle => 'Shared y-axis'; + + @override + String get demoSharedYAxisAlbumCount => '268 albums'; + + @override + String get demoSharedYAxisAlphabeticalSortTitle => 'A-Z'; + + @override + String get demoSharedYAxisRecentSortTitle => 'Recently played'; + + @override + String get demoSharedYAxisAlbumTileTitle => 'Album'; + + @override + String get demoSharedYAxisAlbumTileSubtitle => 'Artist'; + + @override + String get demoSharedYAxisAlbumTileDurationUnit => 'min'; + + @override + String get demoSharedZAxisTitle => 'Shared z-axis'; + + @override + String get demoSharedZAxisSettingsPageTitle => 'Settings'; + + @override + String get demoSharedZAxisBurgerRecipeTitle => 'Burger'; + + @override + String get demoSharedZAxisBurgerRecipeDescription => 'Burger recipe'; + + @override + String get demoSharedZAxisSandwichRecipeTitle => 'Sandwich'; + + @override + String get demoSharedZAxisSandwichRecipeDescription => 'Sandwich recipe'; + + @override + String get demoSharedZAxisDessertRecipeTitle => 'Dessert'; + + @override + String get demoSharedZAxisDessertRecipeDescription => 'Dessert recipe'; + + @override + String get demoSharedZAxisShrimpPlateRecipeTitle => 'Shrimp'; + + @override + String get demoSharedZAxisShrimpPlateRecipeDescription => 'Shrimp plate recipe'; + + @override + String get demoSharedZAxisCrabPlateRecipeTitle => 'Crab'; + + @override + String get demoSharedZAxisCrabPlateRecipeDescription => 'Crab plate recipe'; + + @override + String get demoSharedZAxisBeefSandwichRecipeTitle => 'Beef Sandwich'; + + @override + String get demoSharedZAxisBeefSandwichRecipeDescription => 'Beef Sandwich recipe'; + + @override + String get demoSharedZAxisSavedRecipesListTitle => 'Saved Recipes'; + + @override + String get demoSharedZAxisProfileSettingLabel => 'Profile'; + + @override + String get demoSharedZAxisNotificationSettingLabel => 'Notifications'; + + @override + String get demoSharedZAxisPrivacySettingLabel => 'Privacy'; + + @override + String get demoSharedZAxisHelpSettingLabel => 'Help'; + + @override + String get demoFadeThroughTitle => 'Fade through'; + + @override + String get demoFadeThroughDescription => 'The fade through pattern is used for transitions between UI elements that do not have a strong relationship to each other.'; + + @override + String get demoFadeThroughAlbumsDestination => 'Albums'; + + @override + String get demoFadeThroughPhotosDestination => 'Photos'; + + @override + String get demoFadeThroughSearchDestination => 'Search'; + + @override + String get demoFadeThroughTextPlaceholder => '123 photos'; + + @override + String get demoFadeScaleTitle => 'Fade'; + + @override + String get demoFadeScaleDescription => 'The fade pattern is used for UI elements that enter or exit within the bounds of the screen, such as a dialog that fades in the center of the screen.'; + + @override + String get demoFadeScaleShowAlertDialogButton => 'SHOW MODAL'; + + @override + String get demoFadeScaleShowFabButton => 'SHOW FAB'; + + @override + String get demoFadeScaleHideFabButton => 'HIDE FAB'; + + @override + String get demoFadeScaleAlertDialogHeader => 'Alert Dialog'; + + @override + String get demoFadeScaleAlertDialogCancelButton => 'CANCEL'; + + @override + String get demoFadeScaleAlertDialogDiscardButton => 'DISCARD'; + + @override + String get demoColorsTitle => 'Colors'; + + @override + String get demoColorsSubtitle => 'All of the predefined colors'; + + @override + String get demoColorsDescription => "Color and color swatch constants which represent Material Design's color palette."; + + @override + String get demoTypographyTitle => 'Typography'; + + @override + String get demoTypographySubtitle => 'All of the predefined text styles'; + + @override + String get demoTypographyDescription => 'Definitions for the various typographical styles found in Material Design.'; + + @override + String get demo2dTransformationsTitle => '2D transformations'; + + @override + String get demo2dTransformationsSubtitle => 'Pan and zoom'; + + @override + String get demo2dTransformationsDescription => 'Tap to edit tiles, and use gestures to move around the scene. Drag to pan and pinch with two fingers to zoom. Press the reset button to return to the starting orientation.'; + + @override + String get demo2dTransformationsResetTooltip => 'Reset transformations'; + + @override + String get demo2dTransformationsEditTooltip => 'Edit tile'; + + @override + String get buttonText => 'BUTTON'; + + @override + String get demoBottomSheetTitle => 'Bottom sheet'; + + @override + String get demoBottomSheetSubtitle => 'Persistent and modal bottom sheets'; + + @override + String get demoBottomSheetPersistentTitle => 'Persistent bottom sheet'; + + @override + String get demoBottomSheetPersistentDescription => 'A persistent bottom sheet shows information that supplements the primary content of the app. A persistent bottom sheet remains visible even when the user interacts with other parts of the app.'; + + @override + String get demoBottomSheetModalTitle => 'Modal bottom sheet'; + + @override + String get demoBottomSheetModalDescription => 'A modal bottom sheet is an alternative to a menu or a dialog and prevents the user from interacting with the rest of the app.'; + + @override + String get demoBottomSheetAddLabel => 'Add'; + + @override + String get demoBottomSheetButtonText => 'SHOW BOTTOM SHEET'; + + @override + String get demoBottomSheetHeader => 'Header'; + + @override + String demoBottomSheetItem(Object value) { + return 'Item $value'; + } + + @override + String get demoListsTitle => 'Lists'; + + @override + String get demoListsSubtitle => 'Scrolling list layouts'; + + @override + String get demoListsDescription => 'A single fixed-height row that typically contains some text as well as a leading or trailing icon.'; + + @override + String get demoOneLineListsTitle => 'One Line'; + + @override + String get demoTwoLineListsTitle => 'Two Lines'; + + @override + String get demoListsSecondary => 'Secondary text'; + + @override + String get demoProgressIndicatorTitle => 'Progress indicators'; + + @override + String get demoProgressIndicatorSubtitle => 'Linear, circular, indeterminate'; + + @override + String get demoCircularProgressIndicatorTitle => 'Circular Progress Indicator'; + + @override + String get demoCircularProgressIndicatorDescription => 'A Material Design circular progress indicator, which spins to indicate that the application is busy.'; + + @override + String get demoLinearProgressIndicatorTitle => 'Linear Progress Indicator'; + + @override + String get demoLinearProgressIndicatorDescription => 'A Material Design linear progress indicator, also known as a progress bar.'; + + @override + String get demoPickersTitle => 'Pickers'; + + @override + String get demoPickersSubtitle => 'Date and time selection'; + + @override + String get demoDatePickerTitle => 'Date Picker'; + + @override + String get demoDatePickerDescription => 'Shows a dialog containing a Material Design date picker.'; + + @override + String get demoTimePickerTitle => 'Time Picker'; + + @override + String get demoTimePickerDescription => 'Shows a dialog containing a Material Design time picker.'; + + @override + String get demoDateRangePickerTitle => 'Date Range Picker'; + + @override + String get demoDateRangePickerDescription => 'Shows a dialog containing a Material Design date range picker.'; + + @override + String get demoPickersShowPicker => 'SHOW PICKER'; + + @override + String get demoTabsTitle => 'Tabs'; + + @override + String get demoTabsScrollingTitle => 'Scrolling'; + + @override + String get demoTabsNonScrollingTitle => 'Non-scrolling'; + + @override + String get demoTabsSubtitle => 'Tabs with independently scrollable views'; + + @override + String get demoTabsDescription => 'Tabs organize content across different screens, data sets, and other interactions.'; + + @override + String get demoSnackbarsTitle => 'Snackbars'; + + @override + String get demoSnackbarsSubtitle => 'Snackbars show messages at the bottom of the screen'; + + @override + String get demoSnackbarsDescription => "Snackbars inform users of a process that an app has performed or will perform. They appear temporarily, towards the bottom of the screen. They shouldn't interrupt the user experience, and they don't require user input to disappear."; + + @override + String get demoSnackbarsButtonLabel => 'SHOW A SNACKBAR'; + + @override + String get demoSnackbarsText => 'This is a snackbar.'; + + @override + String get demoSnackbarsActionButtonLabel => 'ACTION'; + + @override + String get demoSnackbarsAction => 'You pressed the snackbar action.'; + + @override + String get demoSelectionControlsTitle => 'Selection controls'; + + @override + String get demoSelectionControlsSubtitle => 'Checkboxes, radio buttons, and switches'; + + @override + String get demoSelectionControlsCheckboxTitle => 'Checkbox'; + + @override + String get demoSelectionControlsCheckboxDescription => "Checkboxes allow the user to select multiple options from a set. A normal checkbox's value is true or false and a tristate checkbox's value can also be null."; + + @override + String get demoSelectionControlsRadioTitle => 'Radio'; + + @override + String get demoSelectionControlsRadioDescription => 'Radio buttons allow the user to select one option from a set. Use radio buttons for exclusive selection if you think that the user needs to see all available options side-by-side.'; + + @override + String get demoSelectionControlsSwitchTitle => 'Switch'; + + @override + String get demoSelectionControlsSwitchDescription => "On/off switches toggle the state of a single settings option. The option that the switch controls, as well as the state it's in, should be made clear from the corresponding inline label."; + + @override + String get demoBottomTextFieldsTitle => 'Text fields'; + + @override + String get demoTextFieldTitle => 'Text fields'; + + @override + String get demoTextFieldSubtitle => 'Single line of editable text and numbers'; + + @override + String get demoTextFieldDescription => 'Text fields allow users to enter text into a UI. They typically appear in forms and dialogs.'; + + @override + String get demoTextFieldShowPasswordLabel => 'Show password'; + + @override + String get demoTextFieldHidePasswordLabel => 'Hide password'; + + @override + String get demoTextFieldFormErrors => 'Please fix the errors in red before submitting.'; + + @override + String get demoTextFieldNameRequired => 'Name is required.'; + + @override + String get demoTextFieldOnlyAlphabeticalChars => 'Please enter only alphabetical characters.'; + + @override + String get demoTextFieldEnterUSPhoneNumber => '(###) ###-#### - Enter a US phone number.'; + + @override + String get demoTextFieldEnterPassword => 'Please enter a password.'; + + @override + String get demoTextFieldPasswordsDoNotMatch => "The passwords don't match"; + + @override + String get demoTextFieldWhatDoPeopleCallYou => 'What do people call you?'; + + @override + String get demoTextFieldNameField => 'Name*'; + + @override + String get demoTextFieldWhereCanWeReachYou => 'Where can we reach you?'; + + @override + String get demoTextFieldPhoneNumber => 'Phone number*'; + + @override + String get demoTextFieldYourEmailAddress => 'Your email address'; + + @override + String get demoTextFieldEmail => 'Email'; + + @override + String get demoTextFieldTellUsAboutYourself => 'Tell us about yourself (e.g., write down what you do or what hobbies you have)'; + + @override + String get demoTextFieldKeepItShort => 'Keep it short, this is just a demo.'; + + @override + String get demoTextFieldLifeStory => 'Life story'; + + @override + String get demoTextFieldSalary => 'Salary'; + + @override + String get demoTextFieldUSD => 'USD'; + + @override + String get demoTextFieldNoMoreThan => 'No more than 8 characters.'; + + @override + String get demoTextFieldPassword => 'Password*'; + + @override + String get demoTextFieldRetypePassword => 'Re-type password*'; + + @override + String get demoTextFieldSubmit => 'SUBMIT'; + + @override + String demoTextFieldNameHasPhoneNumber(Object name, Object phoneNumber) { + return '$name phone number is $phoneNumber'; + } + + @override + String get demoTextFieldRequiredField => '* indicates required field'; + + @override + String get demoTooltipTitle => 'Tooltips'; + + @override + String get demoTooltipSubtitle => 'Short message displayed on long press or hover'; + + @override + String get demoTooltipDescription => 'Tooltips provide text labels that help explain the function of a button or other user interface action. Tooltips display informative text when users hover over, focus on, or long press an element.'; + + @override + String get demoTooltipInstructions => 'Long press or hover to display the tooltip.'; + + @override + String get bottomNavigationCommentsTab => 'Comments'; + + @override + String get bottomNavigationCalendarTab => 'Calendar'; + + @override + String get bottomNavigationAccountTab => 'Account'; + + @override + String get bottomNavigationAlarmTab => 'Alarm'; + + @override + String get bottomNavigationCameraTab => 'Camera'; + + @override + String bottomNavigationContentPlaceholder(Object title) { + return 'Placeholder for $title tab'; + } + + @override + String get buttonTextCreate => 'Create'; + + @override + String dialogSelectedOption(Object value) { + return 'You selected: "$value"'; + } + + @override + String get chipTurnOnLights => 'Turn on lights'; + + @override + String get chipSmall => 'Small'; + + @override + String get chipMedium => 'Medium'; + + @override + String get chipLarge => 'Large'; + + @override + String get chipElevator => 'Elevator'; + + @override + String get chipWasher => 'Washer'; + + @override + String get chipFireplace => 'Fireplace'; + + @override + String get chipBiking => 'Biking'; + + @override + String get demo => 'Demo'; + + @override + String get bottomAppBar => 'Bottom app bar'; + + @override + String get loading => 'Loading'; + + @override + String get dialogDiscardTitle => 'Discard draft?'; + + @override + String get dialogLocationTitle => "Use Google's location service?"; + + @override + String get dialogLocationDescription => 'Let Google help apps determine location. This means sending anonymous location data to Google, even when no apps are running.'; + + @override + String get dialogCancel => 'CANCEL'; + + @override + String get dialogDiscard => 'DISCARD'; + + @override + String get dialogDisagree => 'DISAGREE'; + + @override + String get dialogAgree => 'AGREE'; + + @override + String get dialogSetBackup => 'Set backup account'; + + @override + String get dialogAddAccount => 'Add account'; + + @override + String get dialogShow => 'SHOW DIALOG'; + + @override + String get dialogFullscreenTitle => 'Full Screen Dialog'; + + @override + String get dialogFullscreenSave => 'SAVE'; + + @override + String get dialogFullscreenDescription => 'A full screen dialog demo'; + + @override + String get cupertinoButton => 'Button'; + + @override + String get cupertinoButtonWithBackground => 'With Background'; + + @override + String get cupertinoAlertCancel => 'Cancel'; + + @override + String get cupertinoAlertDiscard => 'Discard'; + + @override + String get cupertinoAlertLocationTitle => 'Allow "Maps" to access your location while you are using the app?'; + + @override + String get cupertinoAlertLocationDescription => 'Your current location will be displayed on the map and used for directions, nearby search results, and estimated travel times.'; + + @override + String get cupertinoAlertAllow => 'Allow'; + + @override + String get cupertinoAlertDontAllow => "Don't Allow"; + + @override + String get cupertinoAlertFavoriteDessert => 'Select Favorite Dessert'; + + @override + String get cupertinoAlertDessertDescription => 'Please select your favorite type of dessert from the list below. Your selection will be used to customize the suggested list of eateries in your area.'; + + @override + String get cupertinoAlertCheesecake => 'Cheesecake'; + + @override + String get cupertinoAlertTiramisu => 'Tiramisu'; + + @override + String get cupertinoAlertApplePie => 'Apple Pie'; + + @override + String get cupertinoAlertChocolateBrownie => 'Chocolate Brownie'; + + @override + String get cupertinoShowAlert => 'Show Alert'; + + @override + String get colorsRed => 'RED'; + + @override + String get colorsPink => 'PINK'; + + @override + String get colorsPurple => 'PURPLE'; + + @override + String get colorsDeepPurple => 'DEEP PURPLE'; + + @override + String get colorsIndigo => 'INDIGO'; + + @override + String get colorsBlue => 'BLUE'; + + @override + String get colorsLightBlue => 'LIGHT BLUE'; + + @override + String get colorsCyan => 'CYAN'; + + @override + String get colorsTeal => 'TEAL'; + + @override + String get colorsGreen => 'GREEN'; + + @override + String get colorsLightGreen => 'LIGHT GREEN'; + + @override + String get colorsLime => 'LIME'; + + @override + String get colorsYellow => 'YELLOW'; + + @override + String get colorsAmber => 'AMBER'; + + @override + String get colorsOrange => 'ORANGE'; + + @override + String get colorsDeepOrange => 'DEEP ORANGE'; + + @override + String get colorsBrown => 'BROWN'; + + @override + String get colorsGrey => 'GREY'; + + @override + String get colorsBlueGrey => 'BLUE GREY'; + + @override + String get placeChennai => 'Chennai'; + + @override + String get placeTanjore => 'Tanjore'; + + @override + String get placeChettinad => 'Chettinad'; + + @override + String get placePondicherry => 'Pondicherry'; + + @override + String get placeFlowerMarket => 'Flower Market'; + + @override + String get placeBronzeWorks => 'Bronze Works'; + + @override + String get placeMarket => 'Market'; + + @override + String get placeThanjavurTemple => 'Thanjavur Temple'; + + @override + String get placeSaltFarm => 'Salt Farm'; + + @override + String get placeScooters => 'Scooters'; + + @override + String get placeSilkMaker => 'Silk Maker'; + + @override + String get placeLunchPrep => 'Lunch Prep'; + + @override + String get placeBeach => 'Beach'; + + @override + String get placeFisherman => 'Fisherman'; + + @override + String get starterAppTitle => 'Starter app'; + + @override + String get starterAppDescription => 'A responsive starter layout'; + + @override + String get starterAppGenericButton => 'BUTTON'; + + @override + String get starterAppTooltipAdd => 'Add'; + + @override + String get starterAppTooltipFavorite => 'Favorite'; + + @override + String get starterAppTooltipShare => 'Share'; + + @override + String get starterAppTooltipSearch => 'Search'; + + @override + String get starterAppGenericTitle => 'Title'; + + @override + String get starterAppGenericSubtitle => 'Subtitle'; + + @override + String get starterAppGenericHeadline => 'Headline'; + + @override + String get starterAppGenericBody => 'Body'; + + @override + String starterAppDrawerItem(Object value) { + return 'Item $value'; + } + + @override + String get shrineMenuCaption => 'MENU'; + + @override + String get shrineCategoryNameAll => 'ALL'; + + @override + String get shrineCategoryNameAccessories => 'ACCESSORIES'; + + @override + String get shrineCategoryNameClothing => 'CLOTHING'; + + @override + String get shrineCategoryNameHome => 'HOME'; + + @override + String get shrineLogoutButtonCaption => 'LOGOUT'; + + @override + String get shrineLoginUsernameLabel => 'Username'; + + @override + String get shrineLoginPasswordLabel => 'Password'; + + @override + String get shrineCancelButtonCaption => 'CANCEL'; + + @override + String get shrineNextButtonCaption => 'NEXT'; + + @override + String get shrineCartPageCaption => 'CART'; + + @override + String shrineProductQuantity(Object quantity) { + return 'Quantity: $quantity'; + } + + @override + String shrineProductPrice(Object price) { + return 'x $price'; + } + + @override + String shrineCartItemCount(num quantity) { + return intl.Intl.pluralLogic( + quantity, + locale: localeName, + other: '$quantity ITEMS', + one: '1 ITEM', + zero: 'NO ITEMS', + ); + } + + @override + String get shrineCartClearButtonCaption => 'CLEAR CART'; + + @override + String get shrineCartTotalCaption => 'TOTAL'; + + @override + String get shrineCartSubtotalCaption => 'Subtotal:'; + + @override + String get shrineCartShippingCaption => 'Shipping:'; + + @override + String get shrineCartTaxCaption => 'Tax:'; + + @override + String get shrineProductVagabondSack => 'Vagabond sack'; + + @override + String get shrineProductStellaSunglasses => 'Stella sunglasses'; + + @override + String get shrineProductWhitneyBelt => 'Whitney belt'; + + @override + String get shrineProductGardenStrand => 'Garden strand'; + + @override + String get shrineProductStrutEarrings => 'Strut earrings'; + + @override + String get shrineProductVarsitySocks => 'Varsity socks'; + + @override + String get shrineProductWeaveKeyring => 'Weave keyring'; + + @override + String get shrineProductGatsbyHat => 'Gatsby hat'; + + @override + String get shrineProductShrugBag => 'Shrug bag'; + + @override + String get shrineProductGiltDeskTrio => 'Gilt desk trio'; + + @override + String get shrineProductCopperWireRack => 'Copper wire rack'; + + @override + String get shrineProductSootheCeramicSet => 'Soothe ceramic set'; + + @override + String get shrineProductHurrahsTeaSet => 'Hurrahs tea set'; + + @override + String get shrineProductBlueStoneMug => 'Blue stone mug'; + + @override + String get shrineProductRainwaterTray => 'Rainwater tray'; + + @override + String get shrineProductChambrayNapkins => 'Chambray napkins'; + + @override + String get shrineProductSucculentPlanters => 'Succulent planters'; + + @override + String get shrineProductQuartetTable => 'Quartet table'; + + @override + String get shrineProductKitchenQuattro => 'Kitchen quattro'; + + @override + String get shrineProductClaySweater => 'Clay sweater'; + + @override + String get shrineProductSeaTunic => 'Sea tunic'; + + @override + String get shrineProductPlasterTunic => 'Plaster tunic'; + + @override + String get shrineProductWhitePinstripeShirt => 'White pinstripe shirt'; + + @override + String get shrineProductChambrayShirt => 'Chambray shirt'; + + @override + String get shrineProductSeabreezeSweater => 'Seabreeze sweater'; + + @override + String get shrineProductGentryJacket => 'Gentry jacket'; + + @override + String get shrineProductNavyTrousers => 'Navy trousers'; + + @override + String get shrineProductWalterHenleyWhite => 'Walter henley (white)'; + + @override + String get shrineProductSurfAndPerfShirt => 'Surf and perf shirt'; + + @override + String get shrineProductGingerScarf => 'Ginger scarf'; + + @override + String get shrineProductRamonaCrossover => 'Ramona crossover'; + + @override + String get shrineProductClassicWhiteCollar => 'Classic white collar'; + + @override + String get shrineProductCeriseScallopTee => 'Cerise scallop tee'; + + @override + String get shrineProductShoulderRollsTee => 'Shoulder rolls tee'; + + @override + String get shrineProductGreySlouchTank => 'Grey slouch tank'; + + @override + String get shrineProductSunshirtDress => 'Sunshirt dress'; + + @override + String get shrineProductFineLinesTee => 'Fine lines tee'; + + @override + String get shrineTooltipSearch => 'Search'; + + @override + String get shrineTooltipSettings => 'Settings'; + + @override + String get shrineTooltipOpenMenu => 'Open menu'; + + @override + String get shrineTooltipCloseMenu => 'Close menu'; + + @override + String get shrineTooltipCloseCart => 'Close cart'; + + @override + String shrineScreenReaderCart(num quantity) { + return intl.Intl.pluralLogic( + quantity, + locale: localeName, + other: 'Shopping cart, $quantity items', + one: 'Shopping cart, 1 item', + zero: 'Shopping cart, no items', + ); + } + + @override + String get shrineScreenReaderProductAddToCart => 'Add to cart'; + + @override + String shrineScreenReaderRemoveProductButton(Object product) { + return 'Remove $product'; + } + + @override + String get shrineTooltipRemoveItem => 'Remove item'; + + @override + String get craneFormDiners => 'Diners'; + + @override + String get craneFormDate => 'Select Date'; + + @override + String get craneFormTime => 'Select Time'; + + @override + String get craneFormLocation => 'Select Location'; + + @override + String get craneFormTravelers => 'Travelers'; + + @override + String get craneFormOrigin => 'Choose Origin'; + + @override + String get craneFormDestination => 'Choose Destination'; + + @override + String get craneFormDates => 'Select Dates'; + + @override + String craneHours(num hours) { + final String temp0 = intl.Intl.pluralLogic( + hours, + locale: localeName, + other: '${hours}h', + one: '1h', + ); + return temp0; + } + + @override + String craneMinutes(num minutes) { + final String temp0 = intl.Intl.pluralLogic( + minutes, + locale: localeName, + other: '${minutes}m', + one: '1m', + ); + return temp0; + } + + @override + String craneFlightDuration(Object hoursShortForm, Object minutesShortForm) { + return '$hoursShortForm $minutesShortForm'; + } + + @override + String get craneFly => 'FLY'; + + @override + String get craneSleep => 'SLEEP'; + + @override + String get craneEat => 'EAT'; + + @override + String get craneFlySubhead => 'Explore Flights by Destination'; + + @override + String get craneSleepSubhead => 'Explore Properties by Destination'; + + @override + String get craneEatSubhead => 'Explore Restaurants by Destination'; + + @override + String craneFlyStops(num numberOfStops) { + final String temp0 = intl.Intl.pluralLogic( + numberOfStops, + locale: localeName, + other: '$numberOfStops stops', + one: '1 stop', + zero: 'Nonstop', + ); + return temp0; + } + + @override + String craneSleepProperties(num totalProperties) { + final String temp0 = intl.Intl.pluralLogic( + totalProperties, + locale: localeName, + other: '$totalProperties Available Properties', + one: '1 Available Properties', + zero: 'No Available Properties', + ); + return temp0; + } + + @override + String craneEatRestaurants(num totalRestaurants) { + final String temp0 = intl.Intl.pluralLogic( + totalRestaurants, + locale: localeName, + other: '$totalRestaurants Restaurants', + one: '1 Restaurant', + zero: 'No Restaurants', + ); + return temp0; + } + + @override + String get craneFly0 => 'Aspen, United States'; + + @override + String get craneFly1 => 'Big Sur, United States'; + + @override + String get craneFly2 => 'Khumbu Valley, Nepal'; + + @override + String get craneFly3 => 'Machu Picchu, Peru'; + + @override + String get craneFly4 => 'Malé, Maldives'; + + @override + String get craneFly5 => 'Vitznau, Switzerland'; + + @override + String get craneFly6 => 'Mexico City, Mexico'; + + @override + String get craneFly7 => 'Mount Rushmore, United States'; + + @override + String get craneFly8 => 'Singapore'; + + @override + String get craneFly9 => 'Havana, Cuba'; + + @override + String get craneFly10 => 'Cairo, Egypt'; + + @override + String get craneFly11 => 'Lisbon, Portugal'; + + @override + String get craneFly12 => 'Napa, United States'; + + @override + String get craneFly13 => 'Bali, Indonesia'; + + @override + String get craneSleep0 => 'Malé, Maldives'; + + @override + String get craneSleep1 => 'Aspen, United States'; + + @override + String get craneSleep2 => 'Machu Picchu, Peru'; + + @override + String get craneSleep3 => 'Havana, Cuba'; + + @override + String get craneSleep4 => 'Vitznau, Switzerland'; + + @override + String get craneSleep5 => 'Big Sur, United States'; + + @override + String get craneSleep6 => 'Napa, United States'; + + @override + String get craneSleep7 => 'Porto, Portugal'; + + @override + String get craneSleep8 => 'Tulum, Mexico'; + + @override + String get craneSleep9 => 'Lisbon, Portugal'; + + @override + String get craneSleep10 => 'Cairo, Egypt'; + + @override + String get craneSleep11 => 'Taipei, Taiwan'; + + @override + String get craneEat0 => 'Naples, Italy'; + + @override + String get craneEat1 => 'Dallas, United States'; + + @override + String get craneEat2 => 'Córdoba, Argentina'; + + @override + String get craneEat3 => 'Portland, United States'; + + @override + String get craneEat4 => 'Paris, France'; + + @override + String get craneEat5 => 'Seoul, South Korea'; + + @override + String get craneEat6 => 'Seattle, United States'; + + @override + String get craneEat7 => 'Nashville, United States'; + + @override + String get craneEat8 => 'Atlanta, United States'; + + @override + String get craneEat9 => 'Madrid, Spain'; + + @override + String get craneEat10 => 'Lisbon, Portugal'; + + @override + String get craneFly0SemanticLabel => 'Chalet in a snowy landscape with evergreen trees'; + + @override + String get craneFly1SemanticLabel => 'Tent in a field'; + + @override + String get craneFly2SemanticLabel => 'Prayer flags in front of snowy mountain'; + + @override + String get craneFly3SemanticLabel => 'Machu Picchu citadel'; + + @override + String get craneFly4SemanticLabel => 'Overwater bungalows'; + + @override + String get craneFly5SemanticLabel => 'Lake-side hotel in front of mountains'; + + @override + String get craneFly6SemanticLabel => 'Aerial view of Palacio de Bellas Artes'; + + @override + String get craneFly7SemanticLabel => 'Mount Rushmore'; + + @override + String get craneFly8SemanticLabel => 'Supertree Grove'; + + @override + String get craneFly9SemanticLabel => 'Man leaning on an antique blue car'; + + @override + String get craneFly10SemanticLabel => 'Al-Azhar Mosque towers during sunset'; + + @override + String get craneFly11SemanticLabel => 'Brick lighthouse at sea'; + + @override + String get craneFly12SemanticLabel => 'Pool with palm trees'; + + @override + String get craneFly13SemanticLabel => 'Sea-side pool with palm trees'; + + @override + String get craneSleep0SemanticLabel => 'Overwater bungalows'; + + @override + String get craneSleep1SemanticLabel => 'Chalet in a snowy landscape with evergreen trees'; + + @override + String get craneSleep2SemanticLabel => 'Machu Picchu citadel'; + + @override + String get craneSleep3SemanticLabel => 'Man leaning on an antique blue car'; + + @override + String get craneSleep4SemanticLabel => 'Lake-side hotel in front of mountains'; + + @override + String get craneSleep5SemanticLabel => 'Tent in a field'; + + @override + String get craneSleep6SemanticLabel => 'Pool with palm trees'; + + @override + String get craneSleep7SemanticLabel => 'Colorful apartments at Riberia Square'; + + @override + String get craneSleep8SemanticLabel => 'Mayan ruins on a cliff above a beach'; + + @override + String get craneSleep9SemanticLabel => 'Brick lighthouse at sea'; + + @override + String get craneSleep10SemanticLabel => 'Al-Azhar Mosque towers during sunset'; + + @override + String get craneSleep11SemanticLabel => 'Taipei 101 skyscraper'; + + @override + String get craneEat0SemanticLabel => 'Pizza in a wood-fired oven'; + + @override + String get craneEat1SemanticLabel => 'Empty bar with diner-style stools'; + + @override + String get craneEat2SemanticLabel => 'Burger'; + + @override + String get craneEat3SemanticLabel => 'Korean taco'; + + @override + String get craneEat4SemanticLabel => 'Chocolate dessert'; + + @override + String get craneEat5SemanticLabel => 'Artsy restaurant seating area'; + + @override + String get craneEat6SemanticLabel => 'Shrimp dish'; + + @override + String get craneEat7SemanticLabel => 'Bakery entrance'; + + @override + String get craneEat8SemanticLabel => 'Plate of crawfish'; + + @override + String get craneEat9SemanticLabel => 'Cafe counter with pastries'; + + @override + String get craneEat10SemanticLabel => 'Woman holding huge pastrami sandwich'; + + @override + String get fortnightlyMenuFrontPage => 'Front Page'; + + @override + String get fortnightlyMenuWorld => 'World'; + + @override + String get fortnightlyMenuUS => 'US'; + + @override + String get fortnightlyMenuPolitics => 'Politics'; + + @override + String get fortnightlyMenuBusiness => 'Business'; + + @override + String get fortnightlyMenuTech => 'Tech'; + + @override + String get fortnightlyMenuScience => 'Science'; + + @override + String get fortnightlyMenuSports => 'Sports'; + + @override + String get fortnightlyMenuTravel => 'Travel'; + + @override + String get fortnightlyMenuCulture => 'Culture'; + + @override + String get fortnightlyTrendingTechDesign => 'TechDesign'; + + @override + String get fortnightlyTrendingReform => 'Reform'; + + @override + String get fortnightlyTrendingHealthcareRevolution => 'HealthcareRevolution'; + + @override + String get fortnightlyTrendingGreenArmy => 'GreenArmy'; + + @override + String get fortnightlyTrendingStocks => 'Stocks'; + + @override + String get fortnightlyLatestUpdates => 'Latest Updates'; + + @override + String get fortnightlyHeadlineHealthcare => 'The Quiet, Yet Powerful Healthcare Revolution'; + + @override + String get fortnightlyHeadlineWar => 'Divided American Lives During War'; + + @override + String get fortnightlyHeadlineGasoline => 'The Future of Gasoline'; + + @override + String get fortnightlyHeadlineArmy => 'Reforming The Green Army From Within'; + + @override + String get fortnightlyHeadlineStocks => 'As Stocks Stagnate, Many Look To Currency'; + + @override + String get fortnightlyHeadlineFabrics => 'Designers Use Tech To Make Futuristic Fabrics'; + + @override + String get fortnightlyHeadlineFeminists => 'Feminists Take On Partisanship'; + + @override + String get fortnightlyHeadlineBees => 'Farmland Bees In Short Supply'; + + @override + String get replyInboxLabel => 'Inbox'; + + @override + String get replyStarredLabel => 'Starred'; + + @override + String get replySentLabel => 'Sent'; + + @override + String get replyTrashLabel => 'Trash'; + + @override + String get replySpamLabel => 'Spam'; + + @override + String get replyDraftsLabel => 'Drafts'; + + @override + String get demoTwoPaneFoldableLabel => 'Foldable'; + + @override + String get demoTwoPaneFoldableDescription => 'This is how TwoPane behaves on a foldable device.'; + + @override + String get demoTwoPaneSmallScreenLabel => 'Small Screen'; + + @override + String get demoTwoPaneSmallScreenDescription => 'This is how TwoPane behaves on a small screen device.'; + + @override + String get demoTwoPaneTabletLabel => 'Tablet / Desktop'; + + @override + String get demoTwoPaneTabletDescription => 'This is how TwoPane behaves on a larger screen like a tablet or desktop.'; + + @override + String get demoTwoPaneTitle => 'TwoPane'; + + @override + String get demoTwoPaneSubtitle => 'Responsive layouts on foldable, large, and small screens'; + + @override + String get splashSelectDemo => 'Select a demo'; + + @override + String get demoTwoPaneList => 'List'; + + @override + String get demoTwoPaneDetails => 'Details'; + + @override + String get demoTwoPaneSelectItem => 'Select an item'; + + @override + String demoTwoPaneItem(Object value) { + return 'Item $value'; + } + + @override + String demoTwoPaneItemDetails(Object value) { + return 'Item $value details'; + } +} + +/// The translations for English, as used in Iceland (`en_IS`). +class GalleryLocalizationsEnIs extends GalleryLocalizationsEn { + GalleryLocalizationsEnIs(): super('en_IS'); + + @override + String githubRepo(Object repoName) { + return '$repoName GitHub repository'; + } + + @override + String aboutDialogDescription(Object repoLink) { + return 'To see the source code for this app, please visit the $repoLink.'; + } + + @override + String get deselect => 'Deselect'; + + @override + String get notSelected => 'Not selected'; + + @override + String get select => 'Select'; + + @override + String get selectable => 'Selectable (long press)'; + + @override + String get selected => 'Selected'; + + @override + String get signIn => 'SIGN IN'; + + @override + String get bannerDemoText => 'Your password was updated on your other device. Please sign in again.'; + + @override + String get bannerDemoResetText => 'Reset the banner'; + + @override + String get bannerDemoMultipleText => 'Multiple actions'; + + @override + String get bannerDemoLeadingText => 'Leading icon'; + + @override + String get dismiss => 'DISMISS'; + + @override + String get backToGallery => 'Back to Gallery'; + + @override + String get cardsDemoExplore => 'Explore'; + + @override + String cardsDemoExploreSemantics(Object destinationName) { + return 'Explore $destinationName'; + } + + @override + String cardsDemoShareSemantics(Object destinationName) { + return 'Share $destinationName'; + } + + @override + String get cardsDemoTappable => 'Tappable'; + + @override + String get cardsDemoTravelDestinationTitle1 => 'Top 10 cities to visit in Tamil Nadu'; + + @override + String get cardsDemoTravelDestinationDescription1 => 'Number 10'; + + @override + String get cardsDemoTravelDestinationCity1 => 'Thanjavur'; + + @override + String get cardsDemoTravelDestinationLocation1 => 'Thanjavur, Tamil Nadu'; + + @override + String get cardsDemoTravelDestinationTitle2 => 'Artisans of Southern India'; + + @override + String get cardsDemoTravelDestinationDescription2 => 'Silk spinners'; + + @override + String get cardsDemoTravelDestinationCity2 => 'Chettinad'; + + @override + String get cardsDemoTravelDestinationLocation2 => 'Sivaganga, Tamil Nadu'; + + @override + String get cardsDemoTravelDestinationTitle3 => 'Brihadisvara Temple'; + + @override + String get cardsDemoTravelDestinationDescription3 => 'Temples'; + + @override + String get homeHeaderGallery => 'Gallery'; + + @override + String get homeHeaderCategories => 'Categories'; + + @override + String get shrineDescription => 'A fashionable retail app'; + + @override + String get fortnightlyDescription => 'A content-focused news app'; + + @override + String get rallyDescription => 'A personal finance app'; + + @override + String get replyDescription => 'An efficient, focused email app'; + + @override + String get rallyAccountDataChecking => 'Current'; + + @override + String get rallyAccountDataHomeSavings => 'Home savings'; + + @override + String get rallyAccountDataCarSavings => 'Car savings'; + + @override + String get rallyAccountDataVacation => 'Holiday'; + + @override + String get rallyAccountDetailDataAnnualPercentageYield => 'Annual percentage yield'; + + @override + String get rallyAccountDetailDataInterestRate => 'Interest rate'; + + @override + String get rallyAccountDetailDataInterestYtd => 'Interest YTD'; + + @override + String get rallyAccountDetailDataInterestPaidLastYear => 'Interest paid last year'; + + @override + String get rallyAccountDetailDataNextStatement => 'Next statement'; + + @override + String get rallyAccountDetailDataAccountOwner => 'Account owner'; + + @override + String get rallyBillDetailTotalAmount => 'Total amount'; + + @override + String get rallyBillDetailAmountPaid => 'Amount paid'; + + @override + String get rallyBillDetailAmountDue => 'Amount due'; + + @override + String get rallyBudgetCategoryCoffeeShops => 'Coffee shops'; + + @override + String get rallyBudgetCategoryGroceries => 'Groceries'; + + @override + String get rallyBudgetCategoryRestaurants => 'Restaurants'; + + @override + String get rallyBudgetCategoryClothing => 'Clothing'; + + @override + String get rallyBudgetDetailTotalCap => 'Total cap'; + + @override + String get rallyBudgetDetailAmountUsed => 'Amount used'; + + @override + String get rallyBudgetDetailAmountLeft => 'Amount left'; + + @override + String get rallySettingsManageAccounts => 'Manage accounts'; + + @override + String get rallySettingsTaxDocuments => 'Tax documents'; + + @override + String get rallySettingsPasscodeAndTouchId => 'Passcode and Touch ID'; + + @override + String get rallySettingsNotifications => 'Notifications'; + + @override + String get rallySettingsPersonalInformation => 'Personal information'; + + @override + String get rallySettingsPaperlessSettings => 'Paperless settings'; + + @override + String get rallySettingsFindAtms => 'Find ATMs'; + + @override + String get rallySettingsHelp => 'Help'; + + @override + String get rallySettingsSignOut => 'Sign out'; + + @override + String get rallyAccountTotal => 'Total'; + + @override + String get rallyBillsDue => 'Due'; + + @override + String get rallyBudgetLeft => 'Left'; + + @override + String get rallyAccounts => 'Accounts'; + + @override + String get rallyBills => 'Bills'; + + @override + String get rallyBudgets => 'Budgets'; + + @override + String get rallyAlerts => 'Alerts'; + + @override + String get rallySeeAll => 'SEE ALL'; + + @override + String get rallyFinanceLeft => 'LEFT'; + + @override + String get rallyTitleOverview => 'OVERVIEW'; + + @override + String get rallyTitleAccounts => 'ACCOUNTS'; + + @override + String get rallyTitleBills => 'BILLS'; + + @override + String get rallyTitleBudgets => 'BUDGETS'; + + @override + String get rallyTitleSettings => 'SETTINGS'; + + @override + String get rallyLoginLoginToRally => 'Log in to Rally'; + + @override + String get rallyLoginNoAccount => "Don't have an account?"; + + @override + String get rallyLoginSignUp => 'SIGN UP'; + + @override + String get rallyLoginUsername => 'Username'; + + @override + String get rallyLoginPassword => 'Password'; + + @override + String get rallyLoginLabelLogin => 'Log in'; + + @override + String get rallyLoginRememberMe => 'Remember me'; + + @override + String get rallyLoginButtonLogin => 'LOGIN'; + + @override + String rallyAlertsMessageHeadsUpShopping(Object percent) { + return "Heads up: you've used up $percent of your shopping budget for this month."; + } + + @override + String rallyAlertsMessageSpentOnRestaurants(Object amount) { + return "You've spent $amount on restaurants this week."; + } + + @override + String rallyAlertsMessageATMFees(Object amount) { + return "You've spent $amount in ATM fees this month"; + } + + @override + String rallyAlertsMessageCheckingAccount(Object percent) { + return 'Good work! Your current account is $percent higher than last month.'; + } + + @override + String rallyAlertsMessageUnassignedTransactions(num count) { + final String temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Increase your potential tax deduction! Assign categories to $count unassigned transactions.', + one: 'Increase your potential tax deduction! Assign categories to 1 unassigned transaction.', + ); + return temp0; + } + + @override + String get rallySeeAllAccounts => 'See all accounts'; + + @override + String get rallySeeAllBills => 'See all bills'; + + @override + String get rallySeeAllBudgets => 'See all budgets'; + + @override + String rallyAccountAmount(Object accountName, Object accountNumber, Object amount) { + return '$accountName account $accountNumber with $amount.'; + } + + @override + String rallyBillAmount(Object billName, Object date, Object amount) { + return '$billName bill due $date for $amount.'; + } + + @override + String rallyBudgetAmount(Object budgetName, Object amountUsed, Object amountTotal, Object amountLeft) { + return '$budgetName budget with $amountUsed used of $amountTotal, $amountLeft left'; + } + + @override + String get craneDescription => 'A personalised travel app'; + + @override + String get homeCategoryReference => 'STYLES AND OTHER'; + + @override + String get demoInvalidURL => "Couldn't display URL:"; + + @override + String get demoOptionsTooltip => 'Options'; + + @override + String get demoInfoTooltip => 'Info'; + + @override + String get demoCodeTooltip => 'Demo code'; + + @override + String get demoDocumentationTooltip => 'API Documentation'; + + @override + String get demoFullscreenTooltip => 'Full screen'; + + @override + String get demoCodeViewerCopyAll => 'COPY ALL'; + + @override + String get demoCodeViewerCopiedToClipboardMessage => 'Copied to clipboard.'; + + @override + String demoCodeViewerFailedToCopyToClipboardMessage(Object error) { + return 'Failed to copy to clipboard: $error'; + } + + @override + String get demoOptionsFeatureTitle => 'View options'; + + @override + String get demoOptionsFeatureDescription => 'Tap here to view available options for this demo.'; + + @override + String get settingsTitle => 'Settings'; + + @override + String get settingsButtonLabel => 'Settings'; + + @override + String get settingsButtonCloseLabel => 'Close settings'; + + @override + String get settingsSystemDefault => 'System'; + + @override + String get settingsTextScaling => 'Text scaling'; + + @override + String get settingsTextScalingSmall => 'Small'; + + @override + String get settingsTextScalingNormal => 'Normal'; + + @override + String get settingsTextScalingLarge => 'Large'; + + @override + String get settingsTextScalingHuge => 'Huge'; + + @override + String get settingsTextDirection => 'Text direction'; + + @override + String get settingsTextDirectionLocaleBased => 'Based on locale'; + + @override + String get settingsTextDirectionLTR => 'LTR'; + + @override + String get settingsTextDirectionRTL => 'RTL'; + + @override + String get settingsLocale => 'Locale'; + + @override + String get settingsPlatformMechanics => 'Platform mechanics'; + + @override + String get settingsTheme => 'Theme'; + + @override + String get settingsDarkTheme => 'Dark'; + + @override + String get settingsLightTheme => 'Light'; + + @override + String get settingsSlowMotion => 'Slow motion'; + + @override + String get settingsAbout => 'About Flutter Gallery'; + + @override + String get settingsFeedback => 'Send feedback'; + + @override + String get settingsAttribution => 'Designed by TOASTER in London'; + + @override + String get demoAppBarTitle => 'App bar'; + + @override + String get demoAppBarSubtitle => 'Displays information and actions relating to the current screen'; + + @override + String get demoAppBarDescription => "The app bar provides content and actions related to the current screen. It's used for branding, screen titles, navigation and actions"; + + @override + String get demoBottomAppBarTitle => 'Bottom app bar'; + + @override + String get demoBottomAppBarSubtitle => 'Displays navigation and actions at the bottom'; + + @override + String get demoBottomAppBarDescription => 'Bottom app bars provide access to a bottom navigation drawer and up to four actions, including the floating action button.'; + + @override + String get bottomAppBarNotch => 'Notch'; + + @override + String get bottomAppBarPosition => 'Floating action button position'; + + @override + String get bottomAppBarPositionDockedEnd => 'Docked - End'; + + @override + String get bottomAppBarPositionDockedCenter => 'Docked - Centre'; + + @override + String get bottomAppBarPositionFloatingEnd => 'Floating - End'; + + @override + String get bottomAppBarPositionFloatingCenter => 'Floating - Centre'; + + @override + String get demoBannerTitle => 'Banner'; + + @override + String get demoBannerSubtitle => 'Displaying a banner within a list'; + + @override + String get demoBannerDescription => 'A banner displays an important, succinct message, and provides actions for users to address (or dismiss the banner). A user action is required for it to be dismissed.'; + + @override + String get demoBottomNavigationTitle => 'Bottom navigation'; + + @override + String get demoBottomNavigationSubtitle => 'Bottom navigation with cross-fading views'; + + @override + String get demoBottomNavigationPersistentLabels => 'Persistent labels'; + + @override + String get demoBottomNavigationSelectedLabel => 'Selected label'; + + @override + String get demoBottomNavigationDescription => 'Bottom navigation bars display three to five destinations at the bottom of a screen. Each destination is represented by an icon and an optional text label. When a bottom navigation icon is tapped, the user is taken to the top-level navigation destination associated with that icon.'; + + @override + String get demoButtonTitle => 'Buttons'; + + @override + String get demoButtonSubtitle => 'Text, elevated, outlined and more'; + + @override + String get demoTextButtonTitle => 'Text button'; + + @override + String get demoTextButtonDescription => 'A text button displays an ink splash on press but does not lift. Use text buttons on toolbars, in dialogues and inline with padding'; + + @override + String get demoElevatedButtonTitle => 'Elevated button'; + + @override + String get demoElevatedButtonDescription => 'Elevated buttons add dimension to mostly flat layouts. They emphasise functions on busy or wide spaces.'; + + @override + String get demoOutlinedButtonTitle => 'Outlined button'; + + @override + String get demoOutlinedButtonDescription => 'Outlined buttons become opaque and elevate when pressed. They are often paired with raised buttons to indicate an alternative, secondary action.'; + + @override + String get demoToggleButtonTitle => 'Toggle Buttons'; + + @override + String get demoToggleButtonDescription => 'Toggle buttons can be used to group related options. To emphasise groups of related toggle buttons, a group should share a common container'; + + @override + String get demoFloatingButtonTitle => 'Floating Action Button'; + + @override + String get demoFloatingButtonDescription => 'A floating action button is a circular icon button that hovers over content to promote a primary action in the application.'; + + @override + String get demoCardTitle => 'Cards'; + + @override + String get demoCardSubtitle => 'Baseline cards with rounded corners'; + + @override + String get demoChipTitle => 'Chips'; + + @override + String get demoCardDescription => 'A card is a sheet of material used to represent some related information, for example, an album, a geographical location, a meal, contact details, etc.'; + + @override + String get demoChipSubtitle => 'Compact elements that represent an input, attribute or action'; + + @override + String get demoActionChipTitle => 'Action chip'; + + @override + String get demoActionChipDescription => 'Action chips are a set of options which trigger an action related to primary content. Action chips should appear dynamically and contextually in a UI.'; + + @override + String get demoChoiceChipTitle => 'Choice chip'; + + @override + String get demoChoiceChipDescription => 'Choice chips represent a single choice from a set. Choice chips contain related descriptive text or categories.'; + + @override + String get demoFilterChipTitle => 'Filter chip'; + + @override + String get demoFilterChipDescription => 'Filter chips use tags or descriptive words as a way to filter content.'; + + @override + String get demoInputChipTitle => 'Input chip'; + + @override + String get demoInputChipDescription => 'Input chips represent a complex piece of information, such as an entity (person, place or thing) or conversational text, in a compact form.'; + + @override + String get demoDataTableTitle => 'Data tables'; + + @override + String get demoDataTableSubtitle => 'Rows and columns of information'; + + @override + String get demoDataTableDescription => "Data tables display information in a grid-like format of rows and columns. They organise information in a way that's easy to scan, so that users can look for patterns and insights."; + + @override + String get dataTableHeader => 'Nutrition'; + + @override + String get dataTableColumnDessert => 'Dessert (1 serving)'; + + @override + String get dataTableColumnCalories => 'Calories'; + + @override + String get dataTableColumnFat => 'Fat (gm)'; + + @override + String get dataTableColumnCarbs => 'Carbs (gm)'; + + @override + String get dataTableColumnProtein => 'Protein (gm)'; + + @override + String get dataTableColumnSodium => 'Sodium (mg)'; + + @override + String get dataTableColumnCalcium => 'Calcium (%)'; + + @override + String get dataTableColumnIron => 'Iron (%)'; + + @override + String get dataTableRowFrozenYogurt => 'Frozen yogurt'; + + @override + String get dataTableRowIceCreamSandwich => 'Ice cream sandwich'; + + @override + String get dataTableRowEclair => 'Eclair'; + + @override + String get dataTableRowCupcake => 'Cupcake'; + + @override + String get dataTableRowGingerbread => 'Gingerbread'; + + @override + String get dataTableRowJellyBean => 'Jelly bean'; + + @override + String get dataTableRowLollipop => 'Lollipop'; + + @override + String get dataTableRowHoneycomb => 'Honeycomb'; + + @override + String get dataTableRowDonut => 'Doughnut'; + + @override + String get dataTableRowApplePie => 'Apple pie'; + + @override + String dataTableRowWithSugar(Object value) { + return '$value with sugar'; + } + + @override + String dataTableRowWithHoney(Object value) { + return '$value with honey'; + } + + @override + String get demoDialogTitle => 'Dialogues'; + + @override + String get demoDialogSubtitle => 'Simple, alert and full-screen'; + + @override + String get demoAlertDialogTitle => 'Alert'; + + @override + String get demoAlertDialogDescription => 'An alert dialogue informs the user about situations that require acknowledgement. An alert dialogue has an optional title and an optional list of actions.'; + + @override + String get demoAlertTitleDialogTitle => 'Alert With Title'; + + @override + String get demoSimpleDialogTitle => 'Simple'; + + @override + String get demoSimpleDialogDescription => 'A simple dialogue offers the user a choice between several options. A simple dialogue has an optional title that is displayed above the choices.'; + + @override + String get demoDividerTitle => 'Divider'; + + @override + String get demoDividerSubtitle => 'A divider is a thin line that groups content in lists and layouts.'; + + @override + String get demoDividerDescription => 'Dividers can be used in lists, drawers and elsewhere to separate content.'; + + @override + String get demoVerticalDividerTitle => 'Vertical divider'; + + @override + String get demoGridListsTitle => 'Grid lists'; + + @override + String get demoGridListsSubtitle => 'Row and column layout'; + + @override + String get demoGridListsDescription => 'Grid lists are best suited for presenting homogeneous data, typically images. Each item in a grid list is called a tile.'; + + @override + String get demoGridListsImageOnlyTitle => 'Image only'; + + @override + String get demoGridListsHeaderTitle => 'With header'; + + @override + String get demoGridListsFooterTitle => 'With footer'; + + @override + String get demoSlidersTitle => 'Sliders'; + + @override + String get demoSlidersSubtitle => 'Widgets for selecting a value by swiping'; + + @override + String get demoSlidersDescription => 'Sliders reflect a range of values along a bar, from which users may select a single value. They are ideal for adjusting settings such as volume, brightness or applying image filters.'; + + @override + String get demoRangeSlidersTitle => 'Range sliders'; + + @override + String get demoRangeSlidersDescription => 'Sliders reflect a range of values along a bar. They can have icons on both ends of the bar that reflect a range of values. They are ideal for adjusting settings such as volume, brightness or applying image filters.'; + + @override + String get demoCustomSlidersTitle => 'Custom sliders'; + + @override + String get demoCustomSlidersDescription => 'Sliders reflect a range of values along a bar, from which users may select a single value or range of values. The sliders can be themed and customised.'; + + @override + String get demoSlidersContinuousWithEditableNumericalValue => 'Continuous with editable numerical value'; + + @override + String get demoSlidersDiscrete => 'Discrete'; + + @override + String get demoSlidersDiscreteSliderWithCustomTheme => 'Discrete slider with custom theme'; + + @override + String get demoSlidersContinuousRangeSliderWithCustomTheme => 'Continuous range slider with custom theme'; + + @override + String get demoSlidersContinuous => 'Continuous'; + + @override + String get demoSlidersEditableNumericalValue => 'Editable numerical value'; + + @override + String get demoMenuTitle => 'Menu'; + + @override + String get demoContextMenuTitle => 'Context menu'; + + @override + String get demoSectionedMenuTitle => 'Sectioned menu'; + + @override + String get demoSimpleMenuTitle => 'Simple menu'; + + @override + String get demoChecklistMenuTitle => 'Checklist menu'; + + @override + String get demoMenuSubtitle => 'Menu buttons and simple menus'; + + @override + String get demoMenuDescription => 'A menu displays a list of choices on a temporary surface. They appear when users interact with a button, action or other control.'; + + @override + String get demoMenuItemValueOne => 'Menu item one'; + + @override + String get demoMenuItemValueTwo => 'Menu item two'; + + @override + String get demoMenuItemValueThree => 'Menu item three'; + + @override + String get demoMenuOne => 'One'; + + @override + String get demoMenuTwo => 'Two'; + + @override + String get demoMenuThree => 'Three'; + + @override + String get demoMenuFour => 'Four'; + + @override + String get demoMenuAnItemWithAContextMenuButton => 'An item with a context menu'; + + @override + String get demoMenuContextMenuItemOne => 'Context menu item one'; + + @override + String get demoMenuADisabledMenuItem => 'Disabled menu item'; + + @override + String get demoMenuContextMenuItemThree => 'Context menu item three'; + + @override + String get demoMenuAnItemWithASectionedMenu => 'An item with a sectioned menu'; + + @override + String get demoMenuPreview => 'Preview'; + + @override + String get demoMenuShare => 'Share'; + + @override + String get demoMenuGetLink => 'Get link'; + + @override + String get demoMenuRemove => 'Remove'; + + @override + String demoMenuSelected(Object value) { + return 'Selected: $value'; + } + + @override + String demoMenuChecked(Object value) { + return 'Checked: $value'; + } + + @override + String get demoNavigationDrawerTitle => 'Navigation drawer'; + + @override + String get demoNavigationDrawerSubtitle => 'Displaying a drawer within app bar'; + + @override + String get demoNavigationDrawerDescription => 'A Material Design panel that slides in horizontally from the edge of the screen to show navigation links in an application.'; + + @override + String get demoNavigationDrawerUserName => 'User name'; + + @override + String get demoNavigationDrawerUserEmail => 'user.name@example.com'; + + @override + String get demoNavigationDrawerToPageOne => 'Item one'; + + @override + String get demoNavigationDrawerToPageTwo => 'Item two'; + + @override + String get demoNavigationDrawerText => 'Swipe from the edge or tap the upper-left icon to see the drawer'; + + @override + String get demoNavigationRailTitle => 'Navigation rail'; + + @override + String get demoNavigationRailSubtitle => 'Displaying a navigation rail within an app'; + + @override + String get demoNavigationRailDescription => 'A material widget that is meant to be displayed at the left or right of an app to navigate between a small number of views, typically between three and five.'; + + @override + String get demoNavigationRailFirst => 'First'; + + @override + String get demoNavigationRailSecond => 'Second'; + + @override + String get demoNavigationRailThird => 'Third'; + + @override + String get demoMenuAnItemWithASimpleMenu => 'An item with a simple menu'; + + @override + String get demoMenuAnItemWithAChecklistMenu => 'An item with a checklist menu'; + + @override + String get demoFullscreenDialogTitle => 'Full screen'; + + @override + String get demoFullscreenDialogDescription => 'The fullscreenDialog property specifies whether the incoming page is a full-screen modal dialogue'; + + @override + String get demoCupertinoActivityIndicatorTitle => 'Activity indicator'; + + @override + String get demoCupertinoActivityIndicatorSubtitle => 'iOS-style activity indicators'; + + @override + String get demoCupertinoActivityIndicatorDescription => 'An iOS-style activity indicator that spins clockwise.'; + + @override + String get demoCupertinoButtonsTitle => 'Buttons'; + + @override + String get demoCupertinoButtonsSubtitle => 'iOS-style buttons'; + + @override + String get demoCupertinoButtonsDescription => 'An iOS-style button. It takes in text and/or an icon that fades out and in on touch. May optionally have a background.'; + + @override + String get demoCupertinoContextMenuTitle => 'Context menu'; + + @override + String get demoCupertinoContextMenuSubtitle => 'iOS-style context menu'; + + @override + String get demoCupertinoContextMenuDescription => 'An iOS-style full screen contextual menu that appears when an element is long-pressed.'; + + @override + String get demoCupertinoContextMenuActionOne => 'Action one'; + + @override + String get demoCupertinoContextMenuActionTwo => 'Action two'; + + @override + String get demoCupertinoContextMenuActionText => 'Tap and hold the Flutter logo to see the context menu.'; + + @override + String get demoCupertinoAlertsTitle => 'Alerts'; + + @override + String get demoCupertinoAlertsSubtitle => 'iOS-style alert dialogues'; + + @override + String get demoCupertinoAlertTitle => 'Alert'; + + @override + String get demoCupertinoAlertDescription => 'An alert dialogue informs the user about situations that require acknowledgement. An alert dialogue has an optional title, optional content and an optional list of actions. The title is displayed above the content and the actions are displayed below the content.'; + + @override + String get demoCupertinoAlertWithTitleTitle => 'Alert with title'; + + @override + String get demoCupertinoAlertButtonsTitle => 'Alert With Buttons'; + + @override + String get demoCupertinoAlertButtonsOnlyTitle => 'Alert Buttons Only'; + + @override + String get demoCupertinoActionSheetTitle => 'Action Sheet'; + + @override + String get demoCupertinoActionSheetDescription => 'An action sheet is a specific style of alert that presents the user with a set of two or more choices related to the current context. An action sheet can have a title, an additional message and a list of actions.'; + + @override + String get demoCupertinoNavigationBarTitle => 'Navigation bar'; + + @override + String get demoCupertinoNavigationBarSubtitle => 'iOS-style navigation bar'; + + @override + String get demoCupertinoNavigationBarDescription => 'An iOS-styled navigation bar. The navigation bar is a toolbar that minimally consists of a page title, in the middle of the toolbar.'; + + @override + String get demoCupertinoPickerTitle => 'Pickers'; + + @override + String get demoCupertinoPickerSubtitle => 'iOS-style pickers'; + + @override + String get demoCupertinoPickerDescription => 'An iOS-style picker widget that can be used to select strings, dates, times or both date and time.'; + + @override + String get demoCupertinoPickerTimer => 'Timer'; + + @override + String get demoCupertinoPicker => 'Picker'; + + @override + String get demoCupertinoPickerDate => 'Date'; + + @override + String get demoCupertinoPickerTime => 'Time'; + + @override + String get demoCupertinoPickerDateTime => 'Date and time'; + + @override + String get demoCupertinoPullToRefreshTitle => 'Pull to refresh'; + + @override + String get demoCupertinoPullToRefreshSubtitle => 'iOS-style pull to refresh control'; + + @override + String get demoCupertinoPullToRefreshDescription => 'A widget implementing the iOS-style pull to refresh content control.'; + + @override + String get demoCupertinoSegmentedControlTitle => 'Segmented control'; + + @override + String get demoCupertinoSegmentedControlSubtitle => 'iOS-style segmented control'; + + @override + String get demoCupertinoSegmentedControlDescription => 'Used to select between a number of mutually exclusive options. When one option in the segmented control is selected, the other options in the segmented control cease to be selected.'; + + @override + String get demoCupertinoSliderTitle => 'Slider'; + + @override + String get demoCupertinoSliderSubtitle => 'iOS-style slider'; + + @override + String get demoCupertinoSliderDescription => 'A slider can be used to select from either a continuous or a discrete set of values.'; + + @override + String demoCupertinoSliderContinuous(Object value) { + return 'Continuous: $value'; + } + + @override + String demoCupertinoSliderDiscrete(Object value) { + return 'Discrete: $value'; + } + + @override + String get demoCupertinoSwitchSubtitle => 'iOS-style switch'; + + @override + String get demoCupertinoSwitchDescription => 'A switch is used to toggle the on/off state of a single setting.'; + + @override + String get demoCupertinoTabBarTitle => 'Tab bar'; + + @override + String get demoCupertinoTabBarSubtitle => 'iOS-style bottom tab bar'; + + @override + String get demoCupertinoTabBarDescription => 'An iOS-style bottom navigation tab bar. Displays multiple tabs with one tab being active, the first tab by default.'; + + @override + String get cupertinoTabBarHomeTab => 'Home'; + + @override + String get cupertinoTabBarChatTab => 'Chat'; + + @override + String get cupertinoTabBarProfileTab => 'Profile'; + + @override + String get demoCupertinoTextFieldTitle => 'Text fields'; + + @override + String get demoCupertinoTextFieldSubtitle => 'iOS-style text fields'; + + @override + String get demoCupertinoTextFieldDescription => 'A text field allows the user to enter text, either with a hardware keyboard or with an on-screen keyboard.'; + + @override + String get demoCupertinoTextFieldPIN => 'PIN'; + + @override + String get demoCupertinoSearchTextFieldTitle => 'Search text field'; + + @override + String get demoCupertinoSearchTextFieldSubtitle => 'iOS-style search text field'; + + @override + String get demoCupertinoSearchTextFieldDescription => 'A search text field that lets the user search by entering text and that can offer and filter suggestions.'; + + @override + String get demoCupertinoSearchTextFieldPlaceholder => 'Enter some text'; + + @override + String get demoCupertinoScrollbarTitle => 'Scrollbar'; + + @override + String get demoCupertinoScrollbarSubtitle => 'iOS-style scrollbar'; + + @override + String get demoCupertinoScrollbarDescription => 'A scrollbar that wraps the given child'; + + @override + String get demoMotionTitle => 'Motion'; + + @override + String get demoMotionSubtitle => 'All of the predefined transition patterns'; + + @override + String get demoContainerTransformDemoInstructions => 'Cards, lists and FAB'; + + @override + String get demoSharedXAxisDemoInstructions => 'Next and back buttons'; + + @override + String get demoSharedYAxisDemoInstructions => "Sort by 'Recently played'"; + + @override + String get demoSharedZAxisDemoInstructions => 'Settings icon button'; + + @override + String get demoFadeThroughDemoInstructions => 'Bottom navigation'; + + @override + String get demoFadeScaleDemoInstructions => 'Modal and FAB'; + + @override + String get demoContainerTransformTitle => 'Container transform'; + + @override + String get demoContainerTransformDescription => 'The container transform pattern is designed for transitions between UI elements that include a container. This pattern creates a visible connection between two UI elements'; + + @override + String get demoContainerTransformModalBottomSheetTitle => 'Fade mode'; + + @override + String get demoContainerTransformTypeFade => 'FADE'; + + @override + String get demoContainerTransformTypeFadeThrough => 'FADE THROUGH'; + + @override + String get demoMotionPlaceholderTitle => 'Title'; + + @override + String get demoMotionPlaceholderSubtitle => 'Secondary text'; + + @override + String get demoMotionSmallPlaceholderSubtitle => 'Secondary'; + + @override + String get demoMotionDetailsPageTitle => 'Details page'; + + @override + String get demoMotionListTileTitle => 'List item'; + + @override + String get demoSharedAxisDescription => 'The shared axis pattern is used for transitions between the UI elements that have a spatial or navigational relationship. This pattern uses a shared transformation on the x, y or z axis to reinforce the relationship between elements.'; + + @override + String get demoSharedXAxisTitle => 'Shared x-axis'; + + @override + String get demoSharedXAxisBackButtonText => 'BACK'; + + @override + String get demoSharedXAxisNextButtonText => 'NEXT'; + + @override + String get demoSharedXAxisCoursePageTitle => 'Streamline your courses'; + + @override + String get demoSharedXAxisCoursePageSubtitle => 'Bundled categories appear as groups in your feed. You can always change this later.'; + + @override + String get demoSharedXAxisArtsAndCraftsCourseTitle => 'Arts and crafts'; + + @override + String get demoSharedXAxisBusinessCourseTitle => 'Business'; + + @override + String get demoSharedXAxisIllustrationCourseTitle => 'Illustration'; + + @override + String get demoSharedXAxisDesignCourseTitle => 'Design'; + + @override + String get demoSharedXAxisCulinaryCourseTitle => 'Culinary'; + + @override + String get demoSharedXAxisBundledCourseSubtitle => 'Bundled'; + + @override + String get demoSharedXAxisIndividualCourseSubtitle => 'Shown individually'; + + @override + String get demoSharedXAxisSignInWelcomeText => 'Hi David Park'; + + @override + String get demoSharedXAxisSignInSubtitleText => 'Sign in with your account'; + + @override + String get demoSharedXAxisSignInTextFieldLabel => 'Email or phone number'; + + @override + String get demoSharedXAxisForgotEmailButtonText => 'FORGOT EMAIL?'; + + @override + String get demoSharedXAxisCreateAccountButtonText => 'CREATE ACCOUNT'; + + @override + String get demoSharedYAxisTitle => 'Shared y-axis'; + + @override + String get demoSharedYAxisAlbumCount => '268 albums'; + + @override + String get demoSharedYAxisAlphabeticalSortTitle => 'A–Z'; + + @override + String get demoSharedYAxisRecentSortTitle => 'Recently played'; + + @override + String get demoSharedYAxisAlbumTileTitle => 'Album'; + + @override + String get demoSharedYAxisAlbumTileSubtitle => 'Artist'; + + @override + String get demoSharedYAxisAlbumTileDurationUnit => 'min'; + + @override + String get demoSharedZAxisTitle => 'Shared z-axis'; + + @override + String get demoSharedZAxisSettingsPageTitle => 'Settings'; + + @override + String get demoSharedZAxisBurgerRecipeTitle => 'Burger'; + + @override + String get demoSharedZAxisBurgerRecipeDescription => 'Burger recipe'; + + @override + String get demoSharedZAxisSandwichRecipeTitle => 'Sandwich'; + + @override + String get demoSharedZAxisSandwichRecipeDescription => 'Sandwich recipe'; + + @override + String get demoSharedZAxisDessertRecipeTitle => 'Dessert'; + + @override + String get demoSharedZAxisDessertRecipeDescription => 'Dessert recipe'; + + @override + String get demoSharedZAxisShrimpPlateRecipeTitle => 'Shrimp'; + + @override + String get demoSharedZAxisShrimpPlateRecipeDescription => 'Shrimp plate recipe'; + + @override + String get demoSharedZAxisCrabPlateRecipeTitle => 'Crab'; + + @override + String get demoSharedZAxisCrabPlateRecipeDescription => 'Crab plate recipe'; + + @override + String get demoSharedZAxisBeefSandwichRecipeTitle => 'Beef sandwich'; + + @override + String get demoSharedZAxisBeefSandwichRecipeDescription => 'Beef sandwich recipe'; + + @override + String get demoSharedZAxisSavedRecipesListTitle => 'Saved recipes'; + + @override + String get demoSharedZAxisProfileSettingLabel => 'Profile'; + + @override + String get demoSharedZAxisNotificationSettingLabel => 'Notifications'; + + @override + String get demoSharedZAxisPrivacySettingLabel => 'Privacy'; + + @override + String get demoSharedZAxisHelpSettingLabel => 'Help'; + + @override + String get demoFadeThroughTitle => 'Fade through'; + + @override + String get demoFadeThroughDescription => 'The fade-through pattern is used for transitions between UI elements that do not have a strong relationship to each other.'; + + @override + String get demoFadeThroughAlbumsDestination => 'Albums'; + + @override + String get demoFadeThroughPhotosDestination => 'Photos'; + + @override + String get demoFadeThroughSearchDestination => 'Search'; + + @override + String get demoFadeThroughTextPlaceholder => '123 photos'; + + @override + String get demoFadeScaleTitle => 'Fade'; + + @override + String get demoFadeScaleDescription => 'The fade pattern is used for UI elements that enter or exit within the bounds of the screen, such as a dialogue that fades in the centre of the screen.'; + + @override + String get demoFadeScaleShowAlertDialogButton => 'SHOW MODAL'; + + @override + String get demoFadeScaleShowFabButton => 'SHOW FAB'; + + @override + String get demoFadeScaleHideFabButton => 'HIDE FAB'; + + @override + String get demoFadeScaleAlertDialogHeader => 'Alert dialogue'; + + @override + String get demoFadeScaleAlertDialogCancelButton => 'CANCEL'; + + @override + String get demoFadeScaleAlertDialogDiscardButton => 'DISCARD'; + + @override + String get demoColorsTitle => 'Colours'; + + @override + String get demoColorsSubtitle => 'All of the predefined colours'; + + @override + String get demoColorsDescription => "Colour and colour swatch constants which represent Material Design's colour palette."; + + @override + String get demoTypographyTitle => 'Typography'; + + @override + String get demoTypographySubtitle => 'All of the predefined text styles'; + + @override + String get demoTypographyDescription => 'Definitions for the various typographical styles found in Material Design.'; + + @override + String get demo2dTransformationsTitle => '2D transformations'; + + @override + String get demo2dTransformationsSubtitle => 'Pan, zoom, rotate'; + + @override + String get demo2dTransformationsDescription => 'Tap to edit tiles, and use gestures to move around the scene. Drag to pan, pinch to zoom, rotate with two fingers. Press the reset button to return to the starting orientation.'; + + @override + String get demo2dTransformationsResetTooltip => 'Reset transformations'; + + @override + String get demo2dTransformationsEditTooltip => 'Edit tile'; + + @override + String get buttonText => 'BUTTON'; + + @override + String get demoBottomSheetTitle => 'Bottom sheet'; + + @override + String get demoBottomSheetSubtitle => 'Persistent and modal bottom sheets'; + + @override + String get demoBottomSheetPersistentTitle => 'Persistent bottom sheet'; + + @override + String get demoBottomSheetPersistentDescription => 'A persistent bottom sheet shows information that supplements the primary content of the app. A persistent bottom sheet remains visible even when the user interacts with other parts of the app.'; + + @override + String get demoBottomSheetModalTitle => 'Modal bottom sheet'; + + @override + String get demoBottomSheetModalDescription => 'A modal bottom sheet is an alternative to a menu or a dialogue and prevents the user from interacting with the rest of the app.'; + + @override + String get demoBottomSheetAddLabel => 'Add'; + + @override + String get demoBottomSheetButtonText => 'SHOW BOTTOM SHEET'; + + @override + String get demoBottomSheetHeader => 'Header'; + + @override + String demoBottomSheetItem(Object value) { + return 'Item $value'; + } + + @override + String get demoListsTitle => 'Lists'; + + @override + String get demoListsSubtitle => 'Scrolling list layouts'; + + @override + String get demoListsDescription => 'A single fixed-height row that typically contains some text as well as a leading or trailing icon.'; + + @override + String get demoOneLineListsTitle => 'One line'; + + @override + String get demoTwoLineListsTitle => 'Two lines'; + + @override + String get demoListsSecondary => 'Secondary text'; + + @override + String get demoProgressIndicatorTitle => 'Progress indicators'; + + @override + String get demoProgressIndicatorSubtitle => 'Linear, circular, indeterminate'; + + @override + String get demoCircularProgressIndicatorTitle => 'Circular progress indicator'; + + @override + String get demoCircularProgressIndicatorDescription => 'A material design circular progress indicator, which spins to indicate that the application is busy.'; + + @override + String get demoLinearProgressIndicatorTitle => 'Linear progress indicator'; + + @override + String get demoLinearProgressIndicatorDescription => 'A material design linear progress indicator, also known as a progress bar.'; + + @override + String get demoPickersTitle => 'Pickers'; + + @override + String get demoPickersSubtitle => 'Date and time selection'; + + @override + String get demoDatePickerTitle => 'Date picker'; + + @override + String get demoDatePickerDescription => 'Shows a dialogue containing a material design date picker.'; + + @override + String get demoTimePickerTitle => 'Time picker'; + + @override + String get demoTimePickerDescription => 'Shows a dialogue containing a material design time picker.'; + + @override + String get demoDateRangePickerTitle => 'Date range picker'; + + @override + String get demoDateRangePickerDescription => 'Shows a dialogue containing a Material Design date range picker.'; + + @override + String get demoPickersShowPicker => 'SHOW PICKER'; + + @override + String get demoTabsTitle => 'Tabs'; + + @override + String get demoTabsScrollingTitle => 'Scrolling'; + + @override + String get demoTabsNonScrollingTitle => 'Non-scrolling'; + + @override + String get demoTabsSubtitle => 'Tabs with independently scrollable views'; + + @override + String get demoTabsDescription => 'Tabs organise content across different screens, data sets and other interactions.'; + + @override + String get demoSnackbarsTitle => 'Snackbars'; + + @override + String get demoSnackbarsSubtitle => 'Snackbars show messages at the bottom of the screen'; + + @override + String get demoSnackbarsDescription => "Snackbars inform users of a process that an app has performed or will perform. They appear temporarily, towards the bottom of the screen. They shouldn't interrupt the user experience, and they don't require user input to disappear."; + + @override + String get demoSnackbarsButtonLabel => 'SHOW A SNACKBAR'; + + @override + String get demoSnackbarsText => 'This is a snackbar.'; + + @override + String get demoSnackbarsActionButtonLabel => 'ACTION'; + + @override + String get demoSnackbarsAction => 'You pressed the snackbar action.'; + + @override + String get demoSelectionControlsTitle => 'Selection controls'; + + @override + String get demoSelectionControlsSubtitle => 'Tick boxes, radio buttons and switches'; + + @override + String get demoSelectionControlsCheckboxTitle => 'Tick box'; + + @override + String get demoSelectionControlsCheckboxDescription => "Tick boxes allow the user to select multiple options from a set. A normal tick box's value is true or false and a tristate tick box's value can also be null."; + + @override + String get demoSelectionControlsRadioTitle => 'Radio'; + + @override + String get demoSelectionControlsRadioDescription => 'Radio buttons allow the user to select one option from a set. Use radio buttons for exclusive selection if you think that the user needs to see all available options side by side.'; + + @override + String get demoSelectionControlsSwitchTitle => 'Switch'; + + @override + String get demoSelectionControlsSwitchDescription => "On/off switches toggle the state of a single settings option. The option that the switch controls, as well as the state it's in, should be made clear from the corresponding inline label."; + + @override + String get demoBottomTextFieldsTitle => 'Text fields'; + + @override + String get demoTextFieldTitle => 'Text fields'; + + @override + String get demoTextFieldSubtitle => 'Single line of editable text and numbers'; + + @override + String get demoTextFieldDescription => 'Text fields allow users to enter text into a UI. They typically appear in forms and dialogues.'; + + @override + String get demoTextFieldShowPasswordLabel => 'Show password'; + + @override + String get demoTextFieldHidePasswordLabel => 'Hide password'; + + @override + String get demoTextFieldFormErrors => 'Please fix the errors in red before submitting.'; + + @override + String get demoTextFieldNameRequired => 'Name is required.'; + + @override + String get demoTextFieldOnlyAlphabeticalChars => 'Please enter only alphabetical characters.'; + + @override + String get demoTextFieldEnterUSPhoneNumber => '(###) ###-#### – Enter a US phone number.'; + + @override + String get demoTextFieldEnterPassword => 'Please enter a password.'; + + @override + String get demoTextFieldPasswordsDoNotMatch => "The passwords don't match"; + + @override + String get demoTextFieldWhatDoPeopleCallYou => 'What do people call you?'; + + @override + String get demoTextFieldNameField => 'Name*'; + + @override + String get demoTextFieldWhereCanWeReachYou => 'Where can we contact you?'; + + @override + String get demoTextFieldPhoneNumber => 'Phone number*'; + + @override + String get demoTextFieldYourEmailAddress => 'Your email address'; + + @override + String get demoTextFieldEmail => 'Email'; + + @override + String get demoTextFieldTellUsAboutYourself => 'Tell us about yourself (e.g. write down what you do or what hobbies you have)'; + + @override + String get demoTextFieldKeepItShort => 'Keep it short, this is just a demo.'; + + @override + String get demoTextFieldLifeStory => 'Life story'; + + @override + String get demoTextFieldSalary => 'Salary'; + + @override + String get demoTextFieldUSD => 'USD'; + + @override + String get demoTextFieldNoMoreThan => 'No more than 8 characters.'; + + @override + String get demoTextFieldPassword => 'Password*'; + + @override + String get demoTextFieldRetypePassword => 'Re-type password*'; + + @override + String get demoTextFieldSubmit => 'SUBMIT'; + + @override + String demoTextFieldNameHasPhoneNumber(Object name, Object phoneNumber) { + return '$name phone number is $phoneNumber'; + } + + @override + String get demoTextFieldRequiredField => '* indicates required field'; + + @override + String get demoTooltipTitle => 'Tooltips'; + + @override + String get demoTooltipSubtitle => 'Short message displayed on long press or hover'; + + @override + String get demoTooltipDescription => 'Tooltips provide text labels that help to explain the function of a button or other user interface action. Tooltips display informative text when users hover over, focus on or long press an element.'; + + @override + String get demoTooltipInstructions => 'Long press or hover to display the tooltip.'; + + @override + String get bottomNavigationCommentsTab => 'Comments'; + + @override + String get bottomNavigationCalendarTab => 'Calendar'; + + @override + String get bottomNavigationAccountTab => 'Account'; + + @override + String get bottomNavigationAlarmTab => 'Alarm'; + + @override + String get bottomNavigationCameraTab => 'Camera'; + + @override + String bottomNavigationContentPlaceholder(Object title) { + return 'Placeholder for $title tab'; + } + + @override + String get buttonTextCreate => 'Create'; + + @override + String dialogSelectedOption(Object value) { + return "You selected: '$value'"; + } + + @override + String get chipTurnOnLights => 'Turn on lights'; + + @override + String get chipSmall => 'Small'; + + @override + String get chipMedium => 'Medium'; + + @override + String get chipLarge => 'Large'; + + @override + String get chipElevator => 'Lift'; + + @override + String get chipWasher => 'Washing machine'; + + @override + String get chipFireplace => 'Fireplace'; + + @override + String get chipBiking => 'Cycling'; + + @override + String get demo => 'Demo'; + + @override + String get bottomAppBar => 'Bottom app bar'; + + @override + String get loading => 'Loading'; + + @override + String get dialogDiscardTitle => 'Discard draft?'; + + @override + String get dialogLocationTitle => "Use Google's location service?"; + + @override + String get dialogLocationDescription => 'Let Google help apps determine location. This means sending anonymous location data to Google, even when no apps are running.'; + + @override + String get dialogCancel => 'CANCEL'; + + @override + String get dialogDiscard => 'DISCARD'; + + @override + String get dialogDisagree => 'DISAGREE'; + + @override + String get dialogAgree => 'AGREE'; + + @override + String get dialogSetBackup => 'Set backup account'; + + @override + String get dialogAddAccount => 'Add account'; + + @override + String get dialogShow => 'SHOW DIALOGUE'; + + @override + String get dialogFullscreenTitle => 'Full-Screen Dialogue'; + + @override + String get dialogFullscreenSave => 'SAVE'; + + @override + String get dialogFullscreenDescription => 'A full-screen dialogue demo'; + + @override + String get cupertinoButton => 'Button'; + + @override + String get cupertinoButtonWithBackground => 'With background'; + + @override + String get cupertinoAlertCancel => 'Cancel'; + + @override + String get cupertinoAlertDiscard => 'Discard'; + + @override + String get cupertinoAlertLocationTitle => "Allow 'Maps' to access your location while you are using the app?"; + + @override + String get cupertinoAlertLocationDescription => 'Your current location will be displayed on the map and used for directions, nearby search results and estimated travel times.'; + + @override + String get cupertinoAlertAllow => 'Allow'; + + @override + String get cupertinoAlertDontAllow => "Don't allow"; + + @override + String get cupertinoAlertFavoriteDessert => 'Select Favourite Dessert'; + + @override + String get cupertinoAlertDessertDescription => 'Please select your favourite type of dessert from the list below. Your selection will be used to customise the suggested list of eateries in your area.'; + + @override + String get cupertinoAlertCheesecake => 'Cheesecake'; + + @override + String get cupertinoAlertTiramisu => 'Tiramisu'; + + @override + String get cupertinoAlertApplePie => 'Apple Pie'; + + @override + String get cupertinoAlertChocolateBrownie => 'Chocolate brownie'; + + @override + String get cupertinoShowAlert => 'Show alert'; + + @override + String get colorsRed => 'RED'; + + @override + String get colorsPink => 'PINK'; + + @override + String get colorsPurple => 'PURPLE'; + + @override + String get colorsDeepPurple => 'DEEP PURPLE'; + + @override + String get colorsIndigo => 'INDIGO'; + + @override + String get colorsBlue => 'BLUE'; + + @override + String get colorsLightBlue => 'LIGHT BLUE'; + + @override + String get colorsCyan => 'CYAN'; + + @override + String get colorsTeal => 'TEAL'; + + @override + String get colorsGreen => 'GREEN'; + + @override + String get colorsLightGreen => 'LIGHT GREEN'; + + @override + String get colorsLime => 'LIME'; + + @override + String get colorsYellow => 'YELLOW'; + + @override + String get colorsAmber => 'AMBER'; + + @override + String get colorsOrange => 'ORANGE'; + + @override + String get colorsDeepOrange => 'DEEP ORANGE'; + + @override + String get colorsBrown => 'BROWN'; + + @override + String get colorsGrey => 'GREY'; + + @override + String get colorsBlueGrey => 'BLUE GREY'; + + @override + String get placeChennai => 'Chennai'; + + @override + String get placeTanjore => 'Tanjore'; + + @override + String get placeChettinad => 'Chettinad'; + + @override + String get placePondicherry => 'Pondicherry'; + + @override + String get placeFlowerMarket => 'Flower market'; + + @override + String get placeBronzeWorks => 'Bronze works'; + + @override + String get placeMarket => 'Market'; + + @override + String get placeThanjavurTemple => 'Thanjavur Temple'; + + @override + String get placeSaltFarm => 'Salt farm'; + + @override + String get placeScooters => 'Scooters'; + + @override + String get placeSilkMaker => 'Silk maker'; + + @override + String get placeLunchPrep => 'Lunch prep'; + + @override + String get placeBeach => 'Beach'; + + @override + String get placeFisherman => 'Fisherman'; + + @override + String get starterAppTitle => 'Starter app'; + + @override + String get starterAppDescription => 'A responsive starter layout'; + + @override + String get starterAppGenericButton => 'BUTTON'; + + @override + String get starterAppTooltipAdd => 'Add'; + + @override + String get starterAppTooltipFavorite => 'Favourite'; + + @override + String get starterAppTooltipShare => 'Share'; + + @override + String get starterAppTooltipSearch => 'Search'; + + @override + String get starterAppGenericTitle => 'Title'; + + @override + String get starterAppGenericSubtitle => 'Subtitle'; + + @override + String get starterAppGenericHeadline => 'Headline'; + + @override + String get starterAppGenericBody => 'Body'; + + @override + String starterAppDrawerItem(Object value) { + return 'Item $value'; + } + + @override + String get shrineMenuCaption => 'MENU'; + + @override + String get shrineCategoryNameAll => 'ALL'; + + @override + String get shrineCategoryNameAccessories => 'ACCESSORIES'; + + @override + String get shrineCategoryNameClothing => 'CLOTHING'; + + @override + String get shrineCategoryNameHome => 'HOME'; + + @override + String get shrineLogoutButtonCaption => 'LOGOUT'; + + @override + String get shrineLoginUsernameLabel => 'Username'; + + @override + String get shrineLoginPasswordLabel => 'Password'; + + @override + String get shrineCancelButtonCaption => 'CANCEL'; + + @override + String get shrineNextButtonCaption => 'NEXT'; + + @override + String get shrineCartPageCaption => 'BASKET'; + + @override + String shrineProductQuantity(Object quantity) { + return 'Quantity: $quantity'; + } + + @override + String shrineProductPrice(Object price) { + return 'x $price'; + } + + @override + String shrineCartItemCount(num quantity) { + final String temp0 = intl.Intl.pluralLogic( + quantity, + locale: localeName, + other: '$quantity ITEMS', + one: '1 ITEM', + zero: 'NO ITEMS', + ); + return temp0; + } + + @override + String get shrineCartClearButtonCaption => 'CLEAR BASKET'; + + @override + String get shrineCartTotalCaption => 'TOTAL'; + + @override + String get shrineCartSubtotalCaption => 'Subtotal:'; + + @override + String get shrineCartShippingCaption => 'Delivery:'; + + @override + String get shrineCartTaxCaption => 'Tax:'; + + @override + String get shrineProductVagabondSack => 'Vagabond sack'; + + @override + String get shrineProductStellaSunglasses => 'Stella sunglasses'; + + @override + String get shrineProductWhitneyBelt => 'Whitney belt'; + + @override + String get shrineProductGardenStrand => 'Garden strand'; + + @override + String get shrineProductStrutEarrings => 'Strut earrings'; + + @override + String get shrineProductVarsitySocks => 'Varsity socks'; + + @override + String get shrineProductWeaveKeyring => 'Weave keyring'; + + @override + String get shrineProductGatsbyHat => 'Gatsby hat'; + + @override + String get shrineProductShrugBag => 'Shrug bag'; + + @override + String get shrineProductGiltDeskTrio => 'Gilt desk trio'; + + @override + String get shrineProductCopperWireRack => 'Copper wire rack'; + + @override + String get shrineProductSootheCeramicSet => 'Soothe ceramic set'; + + @override + String get shrineProductHurrahsTeaSet => 'Hurrahs tea set'; + + @override + String get shrineProductBlueStoneMug => 'Blue stone mug'; + + @override + String get shrineProductRainwaterTray => 'Rainwater tray'; + + @override + String get shrineProductChambrayNapkins => 'Chambray napkins'; + + @override + String get shrineProductSucculentPlanters => 'Succulent planters'; + + @override + String get shrineProductQuartetTable => 'Quartet table'; + + @override + String get shrineProductKitchenQuattro => 'Kitchen quattro'; + + @override + String get shrineProductClaySweater => 'Clay sweater'; + + @override + String get shrineProductSeaTunic => 'Sea tunic'; + + @override + String get shrineProductPlasterTunic => 'Plaster tunic'; + + @override + String get shrineProductWhitePinstripeShirt => 'White pinstripe shirt'; + + @override + String get shrineProductChambrayShirt => 'Chambray shirt'; + + @override + String get shrineProductSeabreezeSweater => 'Seabreeze sweater'; + + @override + String get shrineProductGentryJacket => 'Gentry jacket'; + + @override + String get shrineProductNavyTrousers => 'Navy trousers'; + + @override + String get shrineProductWalterHenleyWhite => 'Walter henley (white)'; + + @override + String get shrineProductSurfAndPerfShirt => 'Surf and perf shirt'; + + @override + String get shrineProductGingerScarf => 'Ginger scarf'; + + @override + String get shrineProductRamonaCrossover => 'Ramona crossover'; + + @override + String get shrineProductClassicWhiteCollar => 'Classic white collar'; + + @override + String get shrineProductCeriseScallopTee => 'Cerise scallop tee'; + + @override + String get shrineProductShoulderRollsTee => 'Shoulder rolls tee'; + + @override + String get shrineProductGreySlouchTank => 'Grey slouch tank top'; + + @override + String get shrineProductSunshirtDress => 'Sunshirt dress'; + + @override + String get shrineProductFineLinesTee => 'Fine lines tee'; + + @override + String get shrineTooltipSearch => 'Search'; + + @override + String get shrineTooltipSettings => 'Settings'; + + @override + String get shrineTooltipOpenMenu => 'Open menu'; + + @override + String get shrineTooltipCloseMenu => 'Close menu'; + + @override + String get shrineTooltipCloseCart => 'Close basket'; + + @override + String shrineScreenReaderCart(num quantity) { + final String temp0 = intl.Intl.pluralLogic( + quantity, + locale: localeName, + other: 'Shopping basket, $quantity items', + one: 'Shopping basket, 1 item', + zero: 'Shopping basket, no items', + ); + return temp0; + } + + @override + String get shrineScreenReaderProductAddToCart => 'Add to basket'; + + @override + String shrineScreenReaderRemoveProductButton(Object product) { + return 'Remove $product'; + } + + @override + String get shrineTooltipRemoveItem => 'Remove item'; + + @override + String get craneFormDiners => 'Diners'; + + @override + String get craneFormDate => 'Select date'; + + @override + String get craneFormTime => 'Select time'; + + @override + String get craneFormLocation => 'Select location'; + + @override + String get craneFormTravelers => 'Travellers'; + + @override + String get craneFormOrigin => 'Choose origin'; + + @override + String get craneFormDestination => 'Choose destination'; + + @override + String get craneFormDates => 'Select dates'; + + @override + String craneHours(num hours) { + final String temp0 = intl.Intl.pluralLogic( + hours, + locale: localeName, + other: '${hours}h', + one: '1 h', + ); + return temp0; + } + + @override + String craneMinutes(num minutes) { + final String temp0 = intl.Intl.pluralLogic( + minutes, + locale: localeName, + other: '${minutes}m', + one: '1 m', + ); + return temp0; + } + + @override + String craneFlightDuration(Object hoursShortForm, Object minutesShortForm) { + return '$hoursShortForm $minutesShortForm'; + } + + @override + String get craneFly => 'FLY'; + + @override + String get craneSleep => 'SLEEP'; + + @override + String get craneEat => 'EAT'; + + @override + String get craneFlySubhead => 'Explore flights by destination'; + + @override + String get craneSleepSubhead => 'Explore properties by destination'; + + @override + String get craneEatSubhead => 'Explore restaurants by destination'; + + @override + String craneFlyStops(num numberOfStops) { + final String temp0 = intl.Intl.pluralLogic( + numberOfStops, + locale: localeName, + other: '$numberOfStops stops', + one: '1 stop', + zero: 'Non-stop', + ); + return temp0; + } + + @override + String craneSleepProperties(num totalProperties) { + final String temp0 = intl.Intl.pluralLogic( + totalProperties, + locale: localeName, + other: '$totalProperties available properties', + one: '1 available property', + zero: 'No available properties', + ); + return temp0; + } + + @override + String craneEatRestaurants(num totalRestaurants) { + return intl.Intl.pluralLogic( + totalRestaurants, + locale: localeName, + other: '$totalRestaurants restaurants', + one: '1 restaurant', + zero: 'No restaurants', + ); + } + + @override + String get craneFly0 => 'Aspen, United States'; + + @override + String get craneFly1 => 'Big Sur, United States'; + + @override + String get craneFly2 => 'Khumbu Valley, Nepal'; + + @override + String get craneFly3 => 'Machu Picchu, Peru'; + + @override + String get craneFly4 => 'Malé, Maldives'; + + @override + String get craneFly5 => 'Vitznau, Switzerland'; + + @override + String get craneFly6 => 'Mexico City, Mexico'; + + @override + String get craneFly7 => 'Mount Rushmore, United States'; + + @override + String get craneFly8 => 'Singapore'; + + @override + String get craneFly9 => 'Havana, Cuba'; + + @override + String get craneFly10 => 'Cairo, Egypt'; + + @override + String get craneFly11 => 'Lisbon, Portugal'; + + @override + String get craneFly12 => 'Napa, United States'; + + @override + String get craneFly13 => 'Bali, Indonesia'; + + @override + String get craneSleep0 => 'Malé, Maldives'; + + @override + String get craneSleep1 => 'Aspen, United States'; + + @override + String get craneSleep2 => 'Machu Picchu, Peru'; + + @override + String get craneSleep3 => 'Havana, Cuba'; + + @override + String get craneSleep4 => 'Vitznau, Switzerland'; + + @override + String get craneSleep5 => 'Big Sur, United States'; + + @override + String get craneSleep6 => 'Napa, United States'; + + @override + String get craneSleep7 => 'Porto, Portugal'; + + @override + String get craneSleep8 => 'Tulum, Mexico'; + + @override + String get craneSleep9 => 'Lisbon, Portugal'; + + @override + String get craneSleep10 => 'Cairo, Egypt'; + + @override + String get craneSleep11 => 'Taipei, Taiwan'; + + @override + String get craneEat0 => 'Naples, Italy'; + + @override + String get craneEat1 => 'Dallas, United States'; + + @override + String get craneEat2 => 'Córdoba, Argentina'; + + @override + String get craneEat3 => 'Portland, United States'; + + @override + String get craneEat4 => 'Paris, France'; + + @override + String get craneEat5 => 'Seoul, South Korea'; + + @override + String get craneEat6 => 'Seattle, United States'; + + @override + String get craneEat7 => 'Nashville, United States'; + + @override + String get craneEat8 => 'Atlanta, United States'; + + @override + String get craneEat9 => 'Madrid, Spain'; + + @override + String get craneEat10 => 'Lisbon, Portugal'; + + @override + String get craneFly0SemanticLabel => 'Chalet in a snowy landscape with evergreen trees'; + + @override + String get craneFly1SemanticLabel => 'Tent in a field'; + + @override + String get craneFly2SemanticLabel => 'Prayer flags in front of snowy mountain'; + + @override + String get craneFly3SemanticLabel => 'Machu Picchu citadel'; + + @override + String get craneFly4SemanticLabel => 'Overwater bungalows'; + + @override + String get craneFly5SemanticLabel => 'Lake-side hotel in front of mountains'; + + @override + String get craneFly6SemanticLabel => 'Aerial view of Palacio de Bellas Artes'; + + @override + String get craneFly7SemanticLabel => 'Mount Rushmore'; + + @override + String get craneFly8SemanticLabel => 'Supertree Grove'; + + @override + String get craneFly9SemanticLabel => 'Man leaning on an antique blue car'; + + @override + String get craneFly10SemanticLabel => 'Al-Azhar Mosque towers during sunset'; + + @override + String get craneFly11SemanticLabel => 'Brick lighthouse at sea'; + + @override + String get craneFly12SemanticLabel => 'Pool with palm trees'; + + @override + String get craneFly13SemanticLabel => 'Seaside pool with palm trees'; + + @override + String get craneSleep0SemanticLabel => 'Overwater bungalows'; + + @override + String get craneSleep1SemanticLabel => 'Chalet in a snowy landscape with evergreen trees'; + + @override + String get craneSleep2SemanticLabel => 'Machu Picchu citadel'; + + @override + String get craneSleep3SemanticLabel => 'Man leaning on an antique blue car'; + + @override + String get craneSleep4SemanticLabel => 'Lake-side hotel in front of mountains'; + + @override + String get craneSleep5SemanticLabel => 'Tent in a field'; + + @override + String get craneSleep6SemanticLabel => 'Pool with palm trees'; + + @override + String get craneSleep7SemanticLabel => 'Colourful apartments at Ribeira Square'; + + @override + String get craneSleep8SemanticLabel => 'Mayan ruins on a cliff above a beach'; + + @override + String get craneSleep9SemanticLabel => 'Brick lighthouse at sea'; + + @override + String get craneSleep10SemanticLabel => 'Al-Azhar Mosque towers during sunset'; + + @override + String get craneSleep11SemanticLabel => 'Taipei 101 skyscraper'; + + @override + String get craneEat0SemanticLabel => 'Pizza in a wood-fired oven'; + + @override + String get craneEat1SemanticLabel => 'Empty bar with diner-style stools'; + + @override + String get craneEat2SemanticLabel => 'Burger'; + + @override + String get craneEat3SemanticLabel => 'Korean taco'; + + @override + String get craneEat4SemanticLabel => 'Chocolate dessert'; + + @override + String get craneEat5SemanticLabel => 'Artsy restaurant seating area'; + + @override + String get craneEat6SemanticLabel => 'Shrimp dish'; + + @override + String get craneEat7SemanticLabel => 'Bakery entrance'; + + @override + String get craneEat8SemanticLabel => 'Plate of crawfish'; + + @override + String get craneEat9SemanticLabel => 'Café counter with pastries'; + + @override + String get craneEat10SemanticLabel => 'Woman holding huge pastrami sandwich'; + + @override + String get fortnightlyMenuFrontPage => 'Front page'; + + @override + String get fortnightlyMenuWorld => 'World'; + + @override + String get fortnightlyMenuUS => 'US'; + + @override + String get fortnightlyMenuPolitics => 'Politics'; + + @override + String get fortnightlyMenuBusiness => 'Business'; + + @override + String get fortnightlyMenuTech => 'Tech'; + + @override + String get fortnightlyMenuScience => 'Science'; + + @override + String get fortnightlyMenuSports => 'Sport'; + + @override + String get fortnightlyMenuTravel => 'Travel'; + + @override + String get fortnightlyMenuCulture => 'Culture'; + + @override + String get fortnightlyTrendingTechDesign => 'TechDesign'; + + @override + String get fortnightlyTrendingReform => 'Reform'; + + @override + String get fortnightlyTrendingHealthcareRevolution => 'HealthcareRevolution'; + + @override + String get fortnightlyTrendingGreenArmy => 'GreenArmy'; + + @override + String get fortnightlyTrendingStocks => 'Stocks'; + + @override + String get fortnightlyLatestUpdates => 'Latest updates'; + + @override + String get fortnightlyHeadlineHealthcare => 'The Quiet, yet Powerful Healthcare Revolution'; + + @override + String get fortnightlyHeadlineWar => 'Divided American Lives During War'; + + @override + String get fortnightlyHeadlineGasoline => 'The Future of Petrol'; + + @override + String get fortnightlyHeadlineArmy => 'Reforming The Green Army from Within'; + + @override + String get fortnightlyHeadlineStocks => 'As Stocks Stagnate, many Look to Currency'; + + @override + String get fortnightlyHeadlineFabrics => 'Designers use Tech to make Futuristic Fabrics'; + + @override + String get fortnightlyHeadlineFeminists => 'Feminists take on Partisanship'; + + @override + String get fortnightlyHeadlineBees => 'Farmland Bees in Short Supply'; + + @override + String get replyInboxLabel => 'Inbox'; + + @override + String get replyStarredLabel => 'Starred'; + + @override + String get replySentLabel => 'Sent'; + + @override + String get replyTrashLabel => 'Bin'; + + @override + String get replySpamLabel => 'Spam'; + + @override + String get replyDraftsLabel => 'Drafts'; + + @override + String get demoTwoPaneFoldableLabel => 'Foldable'; + + @override + String get demoTwoPaneFoldableDescription => 'This is how TwoPane behaves on a foldable device.'; + + @override + String get demoTwoPaneSmallScreenLabel => 'Small screen'; + + @override + String get demoTwoPaneSmallScreenDescription => 'This is how TwoPane behaves on a small screen device.'; + + @override + String get demoTwoPaneTabletLabel => 'Tablet/Desktop'; + + @override + String get demoTwoPaneTabletDescription => 'This is how TwoPane behaves on a larger screen like a tablet or desktop.'; + + @override + String get demoTwoPaneTitle => 'TwoPane'; + + @override + String get demoTwoPaneSubtitle => 'Responsive layouts on foldable, large and small screens'; + + @override + String get splashSelectDemo => 'Select a demo'; + + @override + String get demoTwoPaneList => 'List'; + + @override + String get demoTwoPaneDetails => 'Details'; + + @override + String get demoTwoPaneSelectItem => 'Select an item'; + + @override + String demoTwoPaneItem(Object value) { + return 'Item $value'; + } + + @override + String demoTwoPaneItemDetails(Object value) { + return 'Item $value details'; + } +} diff --git a/dev/integration_tests/new_gallery/lib/l10n/intl_en.arb b/dev/integration_tests/new_gallery/lib/l10n/intl_en.arb new file mode 100644 index 0000000000..ce8f755e9c --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/l10n/intl_en.arb @@ -0,0 +1,3426 @@ +{ + "githubRepo": "{repoName} GitHub repository", + "@githubRepo": { + "description": "Represents a link to a GitHub repository.", + "placeholders": { + "repoName": { + "example": "Flutter Gallery" + } + } + }, + "aboutDialogDescription": "To see the source code for this app, please visit the {repoLink}.", + "@aboutDialogDescription": { + "description": "A description about how to view the source code for this app.", + "placeholders": { + "repoLink": { + "example": "Flutter Gallery GitHub repository" + } + } + }, + "deselect": "Deselect", + "@deselect": { + "description": "Deselect a (selectable) item" + }, + "notSelected": "Not selected", + "@notSelected": { + "description": "Indicates the status of a (selectable) item not being selected" + }, + "select": "Select", + "@select": { + "description": "Select a (selectable) item" + }, + "selectable": "Selectable (long press)", + "@selectable": { + "description": "Indicates the associated piece of UI is selectable by long pressing it" + }, + "selected": "Selected", + "@selected": { + "description": "Indicates status of a (selectable) item being selected" + }, + "signIn": "SIGN IN", + "@signIn": { + "description": "Sign in label to sign into website." + }, + "bannerDemoText": "Your password was updated on your other device. Please sign in again.", + "@bannerDemoText": { + "description": "Password was updated on a different device and the user is required to sign in again" + }, + "bannerDemoResetText": "Reset the banner", + "@bannerDemoResetText": { + "description": "Show the Banner to the user again." + }, + "bannerDemoMultipleText": "Multiple actions", + "@bannerDemoMultipleText": { + "description": "When the user clicks this button the Banner will toggle multiple actions or a single action" + }, + "bannerDemoLeadingText": "Leading Icon", + "@bannerDemoLeadingText": { + "description": "If user clicks this button the leading icon in the Banner will disappear" + }, + "dismiss": "DISMISS", + "@dismiss": { + "description": "When text is pressed the banner widget will be removed from the screen." + }, + "backToGallery": "Back to Gallery", + "@backToGallery": { + "description": "Semantic label for back button to exit a study and return to the gallery home page." + }, + "cardsDemoExplore": "Explore", + "@cardsDemoExplore": { + "description": "Click to see more about the content in the cards demo." + }, + "cardsDemoExploreSemantics": "Explore {destinationName}", + "@cardsDemoExploreSemantics": { + "description": "Semantics label for Explore. Label tells user to explore the destinationName to the user. Example Explore Tamil", + "placeholders": { + "destinationName": { + "example": "Tamil" + } + } + }, + "cardsDemoShareSemantics": "Share {destinationName}", + "@cardsDemoShareSemantics": { + "description": "Semantics label for Share. Label tells user to share the destinationName to the user. Example Share Tamil", + "placeholders": { + "destinationName": { + "example": "Tamil" + } + } + }, + "cardsDemoTappable": "Tappable", + "@cardsDemoTappable": { + "description": "The user can tap this button" + }, + "cardsDemoTravelDestinationTitle1": "Top 10 Cities to Visit in Tamil Nadu", + "@cardsDemoTravelDestinationTitle1": { + "description": "The top 10 cities that you can visit in Tamil Nadu" + }, + "cardsDemoTravelDestinationDescription1": "Number 10", + "@cardsDemoTravelDestinationDescription1": { + "description": "Number 10" + }, + "cardsDemoTravelDestinationCity1": "Thanjavur", + "@cardsDemoTravelDestinationCity1": { + "description": "Thanjavur the city" + }, + "cardsDemoTravelDestinationLocation1": "Thanjavur, Tamil Nadu", + "@cardsDemoTravelDestinationLocation1": { + "description": "Thanjavur, Tamil Nadu is a location" + }, + "cardsDemoTravelDestinationTitle2": "Artisans of Southern India", + "@cardsDemoTravelDestinationTitle2": { + "description": "Artist that are from Southern India" + }, + "cardsDemoTravelDestinationDescription2": "Silk Spinners", + "@cardsDemoTravelDestinationDescription2": { + "description": "Silk Spinners" + }, + "cardsDemoTravelDestinationCity2": "Chettinad", + "@cardsDemoTravelDestinationCity2": { + "description": "Chettinad the city" + }, + "cardsDemoTravelDestinationLocation2": "Sivaganga, Tamil Nadu", + "@cardsDemoTravelDestinationLocation2": { + "description": "Sivaganga, Tamil Nadu is a location" + }, + "cardsDemoTravelDestinationTitle3": "Brihadisvara Temple", + "@cardsDemoTravelDestinationTitle3": { + "description": "Brihadisvara Temple" + }, + "cardsDemoTravelDestinationDescription3": "Temples", + "@cardsDemoTravelDestinationDescription3": { + "description": "Temples" + }, + "homeHeaderGallery": "Gallery", + "@homeHeaderGallery": { + "description": "Header title on home screen for Gallery section." + }, + "homeHeaderCategories": "Categories", + "@homeHeaderCategories": { + "description": "Header title on home screen for Categories section." + }, + "shrineDescription": "A fashionable retail app", + "@shrineDescription": { + "description": "Study description for Shrine." + }, + "fortnightlyDescription": "A content-focused news app", + "@fortnightlyDescription": { + "description": "Study description for Fortnightly." + }, + "rallyDescription": "A personal finance app", + "@rallyDescription": { + "description": "Study description for Rally." + }, + "replyDescription": "An efficient, focused email app", + "@replyDescription": { + "description": "Study description for Reply." + }, + "rallyAccountDataChecking": "Checking", + "@rallyAccountDataChecking": { + "description": "Name for account made up by user." + }, + "rallyAccountDataHomeSavings": "Home Savings", + "@rallyAccountDataHomeSavings": { + "description": "Name for account made up by user." + }, + "rallyAccountDataCarSavings": "Car Savings", + "@rallyAccountDataCarSavings": { + "description": "Name for account made up by user." + }, + "rallyAccountDataVacation": "Vacation", + "@rallyAccountDataVacation": { + "description": "Name for account made up by user." + }, + "rallyAccountDetailDataAnnualPercentageYield": "Annual Percentage Yield", + "@rallyAccountDetailDataAnnualPercentageYield": { + "description": "Title for account statistics. Below a percentage such as 0.10% will be displayed." + }, + "rallyAccountDetailDataInterestRate": "Interest Rate", + "@rallyAccountDetailDataInterestRate": { + "description": "Title for account statistics. Below a dollar amount such as $100 will be displayed." + }, + "rallyAccountDetailDataInterestYtd": "Interest YTD", + "@rallyAccountDetailDataInterestYtd": { + "description": "Title for account statistics. Below a dollar amount such as $100 will be displayed." + }, + "rallyAccountDetailDataInterestPaidLastYear": "Interest Paid Last Year", + "@rallyAccountDetailDataInterestPaidLastYear": { + "description": "Title for account statistics. Below a dollar amount such as $100 will be displayed." + }, + "rallyAccountDetailDataNextStatement": "Next Statement", + "@rallyAccountDetailDataNextStatement": { + "description": "Title for an account detail. Below a date for when the next account statement is released." + }, + "rallyAccountDetailDataAccountOwner": "Account Owner", + "@rallyAccountDetailDataAccountOwner": { + "description": "Title for an account detail. Below the name of the account owner will be displayed." + }, + "rallyBillDetailTotalAmount": "Total Amount", + "@rallyBillDetailTotalAmount": { + "description": "Title for column where it displays the total dollar amount that the user has in bills." + }, + "rallyBillDetailAmountPaid": "Amount Paid", + "@rallyBillDetailAmountPaid": { + "description": "Title for column where it displays the amount that the user has paid." + }, + "rallyBillDetailAmountDue": "Amount Due", + "@rallyBillDetailAmountDue": { + "description": "Title for column where it displays the amount that the user has due." + }, + "rallyBudgetCategoryCoffeeShops": "Coffee Shops", + "@rallyBudgetCategoryCoffeeShops": { + "description": "Category for budget, to sort expenses / bills in." + }, + "rallyBudgetCategoryGroceries": "Groceries", + "@rallyBudgetCategoryGroceries": { + "description": "Category for budget, to sort expenses / bills in." + }, + "rallyBudgetCategoryRestaurants": "Restaurants", + "@rallyBudgetCategoryRestaurants": { + "description": "Category for budget, to sort expenses / bills in." + }, + "rallyBudgetCategoryClothing": "Clothing", + "@rallyBudgetCategoryClothing": { + "description": "Category for budget, to sort expenses / bills in." + }, + "rallyBudgetDetailTotalCap": "Total Cap", + "@rallyBudgetDetailTotalCap": { + "description": "Title for column where it displays the total dollar cap that the user has for its budget." + }, + "rallyBudgetDetailAmountUsed": "Amount Used", + "@rallyBudgetDetailAmountUsed": { + "description": "Title for column where it displays the dollar amount that the user has used in its budget." + }, + "rallyBudgetDetailAmountLeft": "Amount Left", + "@rallyBudgetDetailAmountLeft": { + "description": "Title for column where it displays the dollar amount that the user has left in its budget." + }, + "rallySettingsManageAccounts": "Manage Accounts", + "@rallySettingsManageAccounts": { + "description": "Link to go to the page 'Manage Accounts." + }, + "rallySettingsTaxDocuments": "Tax Documents", + "@rallySettingsTaxDocuments": { + "description": "Link to go to the page 'Tax Documents'." + }, + "rallySettingsPasscodeAndTouchId": "Passcode and Touch ID", + "@rallySettingsPasscodeAndTouchId": { + "description": "Link to go to the page 'Passcode and Touch ID'." + }, + "rallySettingsNotifications": "Notifications", + "@rallySettingsNotifications": { + "description": "Link to go to the page 'Notifications'." + }, + "rallySettingsPersonalInformation": "Personal Information", + "@rallySettingsPersonalInformation": { + "description": "Link to go to the page 'Personal Information'." + }, + "rallySettingsPaperlessSettings": "Paperless Settings", + "@rallySettingsPaperlessSettings": { + "description": "Link to go to the page 'Paperless Settings'." + }, + "rallySettingsFindAtms": "Find ATMs", + "@rallySettingsFindAtms": { + "description": "Link to go to the page 'Find ATMs'." + }, + "rallySettingsHelp": "Help", + "@rallySettingsHelp": { + "description": "Link to go to the page 'Help'." + }, + "rallySettingsSignOut": "Sign out", + "@rallySettingsSignOut": { + "description": "Link to go to the page 'Sign out'." + }, + "rallyAccountTotal": "Total", + "@rallyAccountTotal": { + "description": "Title for 'total account value' overview page, a dollar value is displayed next to it." + }, + "rallyBillsDue": "Due", + "@rallyBillsDue": { + "description": "Title for 'bills due' page, a dollar value is displayed next to it." + }, + "rallyBudgetLeft": "Left", + "@rallyBudgetLeft": { + "description": "Title for 'budget left' page, a dollar value is displayed next to it." + }, + "rallyAccounts": "Accounts", + "@rallyAccounts": { + "description": "Link text for accounts page." + }, + "rallyBills": "Bills", + "@rallyBills": { + "description": "Link text for bills page." + }, + "rallyBudgets": "Budgets", + "@rallyBudgets": { + "description": "Link text for budgets page." + }, + "rallyAlerts": "Alerts", + "@rallyAlerts": { + "description": "Title for alerts part of overview page." + }, + "rallySeeAll": "SEE ALL", + "@rallySeeAll": { + "description": "Link text for button to see all data for category." + }, + "rallyFinanceLeft": " LEFT", + "@rallyFinanceLeft": { + "description": "Displayed as 'dollar amount left', for example $46.70 LEFT, for a budget category." + }, + "rallyTitleOverview": "OVERVIEW", + "@rallyTitleOverview": { + "description": "The navigation link to the overview page." + }, + "rallyTitleAccounts": "ACCOUNTS", + "@rallyTitleAccounts": { + "description": "The navigation link to the accounts page." + }, + "rallyTitleBills": "BILLS", + "@rallyTitleBills": { + "description": "The navigation link to the bills page." + }, + "rallyTitleBudgets": "BUDGETS", + "@rallyTitleBudgets": { + "description": "The navigation link to the budgets page." + }, + "rallyTitleSettings": "SETTINGS", + "@rallyTitleSettings": { + "description": "The navigation link to the settings page." + }, + "rallyLoginLoginToRally": "Login to Rally", + "@rallyLoginLoginToRally": { + "description": "Title for login page for the Rally app (Rally does not need to be translated as it is a product name)." + }, + "rallyLoginNoAccount": "Don't have an account?", + "@rallyLoginNoAccount": { + "description": "Prompt for signing up for an account." + }, + "rallyLoginSignUp": "SIGN UP", + "@rallyLoginSignUp": { + "description": "Button text to sign up for an account." + }, + "rallyLoginUsername": "Username", + "@rallyLoginUsername": { + "description": "The username field in an login form." + }, + "rallyLoginPassword": "Password", + "@rallyLoginPassword": { + "description": "The password field in an login form." + }, + "rallyLoginLabelLogin": "Login", + "@rallyLoginLabelLogin": { + "description": "The label text to login." + }, + "rallyLoginRememberMe": "Remember Me", + "@rallyLoginRememberMe": { + "description": "Text if the user wants to stay logged in." + }, + "rallyLoginButtonLogin": "LOGIN", + "@rallyLoginButtonLogin": { + "description": "Text for login button." + }, + "rallyAlertsMessageHeadsUpShopping": "Heads up, you've used up {percent} of your Shopping budget for this month.", + "@rallyAlertsMessageHeadsUpShopping": { + "description": "Alert message shown when for example, user has used more than 90% of their shopping budget.", + "placeholders": { + "percent": { + "example": "90%" + } + } + }, + "rallyAlertsMessageSpentOnRestaurants": "You've spent {amount} on Restaurants this week.", + "@rallyAlertsMessageSpentOnRestaurants": { + "description": "Alert message shown when for example, user has spent $120 on Restaurants this week.", + "placeholders": { + "amount": { + "example": "$120" + } + } + }, + "rallyAlertsMessageATMFees": "You've spent {amount} in ATM fees this month", + "@rallyAlertsMessageATMFees": { + "description": "Alert message shown when for example, the user has spent $24 in ATM fees this month.", + "placeholders": { + "amount": { + "example": "24" + } + } + }, + "rallyAlertsMessageCheckingAccount": "Good work! Your checking account is {percent} higher than last month.", + "@rallyAlertsMessageCheckingAccount": { + "description": "Alert message shown when for example, the checking account is 1% higher than last month.", + "placeholders": { + "percent": { + "example": "1%" + } + } + }, + "rallyAlertsMessageUnassignedTransactions": "{count, plural, =1{Increase your potential tax deduction! Assign categories to 1 unassigned transaction.}other{Increase your potential tax deduction! Assign categories to {count} unassigned transactions.}}", + "@rallyAlertsMessageUnassignedTransactions": { + "description": "Alert message shown when you have unassigned transactions.", + "placeholders": { + "count": { + "example": "2" + } + } + }, + "rallySeeAllAccounts": "See all accounts", + "@rallySeeAllAccounts": { + "description": "Semantics label for button to see all accounts. Accounts refer to bank account here." + }, + "rallySeeAllBills": "See all bills", + "@rallySeeAllBills": { + "description": "Semantics label for button to see all bills." + }, + "rallySeeAllBudgets": "See all budgets", + "@rallySeeAllBudgets": { + "description": "Semantics label for button to see all budgets." + }, + "rallyAccountAmount": "{accountName} account {accountNumber} with {amount}.", + "@rallyAccountAmount": { + "description": "Semantics label for row with bank account name (for example checking) and its bank account number (for example 123), with how much money is deposited in it (for example $12).", + "placeholders": { + "accountName": { + "example": "Home Savings" + }, + "accountNumber": { + "example": "1234" + }, + "amount": { + "example": "$12" + } + } + }, + "rallyBillAmount": "{billName} bill due {date} for {amount}.", + "@rallyBillAmount": { + "description": "Semantics label for row with a bill (example name is rent), when the bill is due (1/12/2019 for example) and for how much money ($12).", + "placeholders": { + "billName": { + "example": "Rent" + }, + "date": { + "example": "1/24/2019" + }, + "amount": { + "example": "$12" + } + } + }, + "rallyBudgetAmount": "{budgetName} budget with {amountUsed} used of {amountTotal}, {amountLeft} left", + "@rallyBudgetAmount": { + "description": "Semantics label for row with a budget (housing budget for example), with how much is used of the budget (for example $5), the total budget (for example $100) and the amount left in the budget (for example $95).", + "placeholders": { + "budgetName": { + "example": "Groceries" + }, + "amountUsed": { + "example": "$5" + }, + "amountTotal": { + "example": "$100" + }, + "amountLeft": { + "example": "$95" + } + } + }, + "craneDescription": "A personalized travel app", + "@craneDescription": { + "description": "Study description for Crane." + }, + "homeCategoryReference": "STYLES & OTHER", + "@homeCategoryReference": { + "description": "Category title on home screen for styles & other demos (for context, the styles demos consist of a color demo and a typography demo)." + }, + "demoInvalidURL": "Couldn't display URL:", + "@demoInvalidURL": { + "description": "Error message when opening the URL for a demo." + }, + "demoOptionsTooltip": "Options", + "@demoOptionsTooltip": { + "description": "Tooltip for options button in a demo." + }, + "demoInfoTooltip": "Info", + "@demoInfoTooltip": { + "description": "Tooltip for info button in a demo." + }, + "demoCodeTooltip": "Demo Code", + "@demoCodeTooltip": { + "description": "Tooltip for demo code button in a demo." + }, + "demoDocumentationTooltip": "API Documentation", + "@demoDocumentationTooltip": { + "description": "Tooltip for API documentation button in a demo." + }, + "demoFullscreenTooltip": "Full Screen", + "@demoFullscreenTooltip": { + "description": "Tooltip for Full Screen button in a demo." + }, + "demoCodeViewerCopyAll": "COPY ALL", + "@demoCodeViewerCopyAll": { + "description": "Caption for a button to copy all text." + }, + "demoCodeViewerCopiedToClipboardMessage": "Copied to clipboard.", + "@demoCodeViewerCopiedToClipboardMessage": { + "description": "A message displayed to the user after clicking the COPY ALL button, if the text is successfully copied to the clipboard." + }, + "demoCodeViewerFailedToCopyToClipboardMessage": "Failed to copy to clipboard: {error}", + "@demoCodeViewerFailedToCopyToClipboardMessage": { + "description": "A message displayed to the user after clicking the COPY ALL button, if the text CANNOT be copied to the clipboard.", + "placeholders": { + "error": { + "example": "Your browser does not have clipboard support." + } + } + }, + "demoOptionsFeatureTitle": "View options", + "@demoOptionsFeatureTitle": { + "description": "Title for an alert that explains what the options button does." + }, + "demoOptionsFeatureDescription": "Tap here to view available options for this demo.", + "@demoOptionsFeatureDescription": { + "description": "Description for an alert that explains what the options button does." + }, + "settingsTitle": "Settings", + "@settingsTitle": { + "description": "Title for the settings screen." + }, + "settingsButtonLabel": "Settings", + "@settingsButtonLabel": { + "description": "Accessibility label for the settings button when settings are not showing." + }, + "settingsButtonCloseLabel": "Close settings", + "@settingsButtonCloseLabel": { + "description": "Accessibility label for the settings button when settings are showing." + }, + "settingsSystemDefault": "System", + "@settingsSystemDefault": { + "description": "Option label to indicate the system default will be used." + }, + "settingsTextScaling": "Text scaling", + "@settingsTextScaling": { + "description": "Title for text scaling setting." + }, + "settingsTextScalingSmall": "Small", + "@settingsTextScalingSmall": { + "description": "Option label for small text scale setting." + }, + "settingsTextScalingNormal": "Normal", + "@settingsTextScalingNormal": { + "description": "Option label for normal text scale setting." + }, + "settingsTextScalingLarge": "Large", + "@settingsTextScalingLarge": { + "description": "Option label for large text scale setting." + }, + "settingsTextScalingHuge": "Huge", + "@settingsTextScalingHuge": { + "description": "Option label for huge text scale setting." + }, + "settingsTextDirection": "Text direction", + "@settingsTextDirection": { + "description": "Title for text direction setting." + }, + "settingsTextDirectionLocaleBased": "Based on locale", + "@settingsTextDirectionLocaleBased": { + "description": "Option label for locale-based text direction setting." + }, + "settingsTextDirectionLTR": "LTR", + "@settingsTextDirectionLTR": { + "description": "Option label for left-to-right text direction setting." + }, + "settingsTextDirectionRTL": "RTL", + "@settingsTextDirectionRTL": { + "description": "Option label for right-to-left text direction setting." + }, + "settingsLocale": "Locale", + "@settingsLocale": { + "description": "Title for locale setting." + }, + "settingsPlatformMechanics": "Platform mechanics", + "@settingsPlatformMechanics": { + "description": "Title for platform mechanics (iOS, Android, macOS, etc.) setting." + }, + "settingsTheme": "Theme", + "@settingsTheme": { + "description": "Title for the theme setting." + }, + "settingsDarkTheme": "Dark", + "@settingsDarkTheme": { + "description": "Title for the dark theme setting." + }, + "settingsLightTheme": "Light", + "@settingsLightTheme": { + "description": "Title for the light theme setting." + }, + "settingsSlowMotion": "Slow motion", + "@settingsSlowMotion": { + "description": "Title for slow motion setting." + }, + "settingsAbout": "About Flutter Gallery", + "@settingsAbout": { + "description": "Title for information button." + }, + "settingsFeedback": "Send feedback", + "@settingsFeedback": { + "description": "Title for feedback button." + }, + "settingsAttribution": "Designed by TOASTER in London", + "@settingsAttribution": { + "description": "Title for attribution (TOASTER is a proper name and should remain in English)." + }, + "demoAppBarTitle": "App bar", + "@demoAppBarTitle": { + "description": "Title for the material App bar component demo." + }, + "demoAppBarSubtitle": "Displays information and actions relating to the current screen", + "@demoAppBarSubtitle": { + "description": "Subtitle for the material App bar component demo." + }, + "demoAppBarDescription": "The App bar provides content and actions related to the current screen. It's used for branding, screen titles, navigation, and actions", + "@demoAppBarDescription": { + "description": "Description for the material App bar component demo." + }, + "demoBottomAppBarTitle": "Bottom app bar", + "@demoBottomAppBarTitle": { + "description": "Title for the material bottom app bar component demo." + }, + "demoBottomAppBarSubtitle": "Displays navigation and actions at the bottom", + "@demoBottomAppBarSubtitle": { + "description": "Subtitle for the material bottom app bar component demo." + }, + "demoBottomAppBarDescription": "Bottom app bars provide access to a bottom navigation drawer and up to four actions, including the floating action button.", + "@demoBottomAppBarDescription": { + "description": "Description for the material bottom app bar component demo." + }, + "bottomAppBarNotch": "Notch", + "@bottomAppBarNotch": { + "description": "A toggle for whether to have a notch (or cutout) in the bottom app bar demo." + }, + "bottomAppBarPosition": "Floating Action Button Position", + "@bottomAppBarPosition": { + "description": "A setting for the position of the floating action button in the bottom app bar demo." + }, + "bottomAppBarPositionDockedEnd": "Docked - End", + "@bottomAppBarPositionDockedEnd": { + "description": "A setting for the position of the floating action button in the bottom app bar that docks the button in the bar and aligns it at the end." + }, + "bottomAppBarPositionDockedCenter": "Docked - Center", + "@bottomAppBarPositionDockedCenter": { + "description": "A setting for the position of the floating action button in the bottom app bar that docks the button in the bar and aligns it in the center." + }, + "bottomAppBarPositionFloatingEnd": "Floating - End", + "@bottomAppBarPositionFloatingEnd": { + "description": "A setting for the position of the floating action button in the bottom app bar that places the button above the bar and aligns it at the end." + }, + "bottomAppBarPositionFloatingCenter": "Floating - Center", + "@bottomAppBarPositionFloatingCenter": { + "description": "A setting for the position of the floating action button in the bottom app bar that places the button above the bar and aligns it in the center." + }, + "demoBannerTitle": "Banner", + "@demoBannerTitle": { + "description": "Title for the material banner component demo." + }, + "demoBannerSubtitle": "Displaying a banner within a list", + "@demoBannerSubtitle": { + "description": "Subtitle for the material banner component demo." + }, + "demoBannerDescription": "A banner displays an important, succinct message, and provides actions for users to address (or dismiss the banner). A user action is required for it to be dismissed.", + "@demoBannerDescription": { + "description": "Description for the material banner component demo." + }, + "demoBottomNavigationTitle": "Bottom navigation", + "@demoBottomNavigationTitle": { + "description": "Title for the material bottom navigation component demo." + }, + "demoBottomNavigationSubtitle": "Bottom navigation with cross-fading views", + "@demoBottomNavigationSubtitle": { + "description": "Subtitle for the material bottom navigation component demo." + }, + "demoBottomNavigationPersistentLabels": "Persistent labels", + "@demoBottomNavigationPersistentLabels": { + "description": "Option title for bottom navigation with persistent labels." + }, + "demoBottomNavigationSelectedLabel": "Selected label", + "@demoBottomNavigationSelectedLabel": { + "description": "Option title for bottom navigation with only a selected label." + }, + "demoBottomNavigationDescription": "Bottom navigation bars display three to five destinations at the bottom of a screen. Each destination is represented by an icon and an optional text label. When a bottom navigation icon is tapped, the user is taken to the top-level navigation destination associated with that icon.", + "@demoBottomNavigationDescription": { + "description": "Description for the material bottom navigation component demo." + }, + "demoButtonTitle": "Buttons", + "@demoButtonTitle": { + "description": "Title for the material buttons component demo." + }, + "demoButtonSubtitle": "Text, elevated, outlined, and more", + "@demoButtonSubtitle": { + "description": "Subtitle for the material buttons component demo." + }, + "demoTextButtonTitle": "Text Button", + "@demoTextButtonTitle": { + "description": "Title for the text button component demo." + }, + "demoTextButtonDescription": "A text button displays an ink splash on press but does not lift. Use text buttons on toolbars, in dialogs and inline with padding", + "@demoTextButtonDescription": { + "description": "Description for the text button component demo." + }, + "demoElevatedButtonTitle": "Elevated Button", + "@demoElevatedButtonTitle": { + "description": "Title for the elevated button component demo." + }, + "demoElevatedButtonDescription": "Elevated buttons add dimension to mostly flat layouts. They emphasize functions on busy or wide spaces.", + "@demoElevatedButtonDescription": { + "description": "Description for the elevated button component demo." + }, + "demoOutlinedButtonTitle": "Outlined Button", + "@demoOutlinedButtonTitle": { + "description": "Title for the outlined button component demo." + }, + "demoOutlinedButtonDescription": "Outlined buttons become opaque and elevate when pressed. They are often paired with raised buttons to indicate an alternative, secondary action.", + "@demoOutlinedButtonDescription": { + "description": "Description for the outlined button component demo." + }, + "demoToggleButtonTitle": "Toggle Buttons", + "@demoToggleButtonTitle": { + "description": "Title for the toggle buttons component demo." + }, + "demoToggleButtonDescription": "Toggle buttons can be used to group related options. To emphasize groups of related toggle buttons, a group should share a common container", + "@demoToggleButtonDescription": { + "description": "Description for the toggle buttons component demo." + }, + "demoFloatingButtonTitle": "Floating Action Button", + "@demoFloatingButtonTitle": { + "description": "Title for the floating action button component demo." + }, + "demoFloatingButtonDescription": "A floating action button is a circular icon button that hovers over content to promote a primary action in the application.", + "@demoFloatingButtonDescription": { + "description": "Description for the floating action button component demo." + }, + "demoCardTitle": "Cards", + "@demoCardTitle": { + "description": "Title for the material cards component demo." + }, + "demoCardSubtitle": "Baseline cards with rounded corners", + "@demoCardSubtitle": { + "description": "Subtitle for the material cards component demo." + }, + "demoChipTitle": "Chips", + "@demoChipTitle": { + "description": "Title for the material chips component demo." + }, + "demoCardDescription": "A card is a sheet of Material used to represent some related information, for example an album, a geographical location, a meal, contact details, etc.", + "@demoCardDescription": { + "description": "Description for the material cards component demo." + }, + "demoChipSubtitle": "Compact elements that represent an input, attribute, or action", + "@demoChipSubtitle": { + "description": "Subtitle for the material chips component demo." + }, + "demoActionChipTitle": "Action Chip", + "@demoActionChipTitle": { + "description": "Title for the action chip component demo." + }, + "demoActionChipDescription": "Action chips are a set of options which trigger an action related to primary content. Action chips should appear dynamically and contextually in a UI.", + "@demoActionChipDescription": { + "description": "Description for the action chip component demo." + }, + "demoChoiceChipTitle": "Choice Chip", + "@demoChoiceChipTitle": { + "description": "Title for the choice chip component demo." + }, + "demoChoiceChipDescription": "Choice chips represent a single choice from a set. Choice chips contain related descriptive text or categories.", + "@demoChoiceChipDescription": { + "description": "Description for the choice chip component demo." + }, + "demoFilterChipTitle": "Filter Chip", + "@demoFilterChipTitle": { + "description": "Title for the filter chip component demo." + }, + "demoFilterChipDescription": "Filter chips use tags or descriptive words as a way to filter content.", + "@demoFilterChipDescription": { + "description": "Description for the filter chip component demo." + }, + "demoInputChipTitle": "Input Chip", + "@demoInputChipTitle": { + "description": "Title for the input chip component demo." + }, + "demoInputChipDescription": "Input chips represent a complex piece of information, such as an entity (person, place, or thing) or conversational text, in a compact form.", + "@demoInputChipDescription": { + "description": "Description for the input chip component demo." + }, + "demoDataTableTitle": "Data Tables", + "@demoDataTableTitle": { + "description": "Title for the material data table component demo." + }, + "demoDataTableSubtitle": "Rows and columns of information", + "@demoDataTableSubtitle": { + "description": "Subtitle for the material data table component demo." + }, + "demoDataTableDescription": "Data tables display information in a grid-like format of rows and columns. They organize information in a way that's easy to scan, so that users can look for patterns and insights.", + "@demoDataTableDescription": { + "description": "Description for the material data table component demo." + }, + "dataTableHeader": "Nutrition", + "@dataTableHeader": { + "description": "Header for the data table component demo about nutrition." + }, + "dataTableColumnDessert": "Dessert (1 serving)", + "@dataTableColumnDessert": { + "description": "Column header for desserts." + }, + "dataTableColumnCalories": "Calories", + "@dataTableColumnCalories": { + "description": "Column header for number of calories." + }, + "dataTableColumnFat": "Fat (g)", + "@dataTableColumnFat": { + "description": "Column header for number of grams of fat." + }, + "dataTableColumnCarbs": "Carbs (g)", + "@dataTableColumnCarbs": { + "description": "Column header for number of grams of carbs." + }, + "dataTableColumnProtein": "Protein (g)", + "@dataTableColumnProtein": { + "description": "Column header for number of grams of protein." + }, + "dataTableColumnSodium": "Sodium (mg)", + "@dataTableColumnSodium": { + "description": "Column header for number of milligrams of sodium." + }, + "dataTableColumnCalcium": "Calcium (%)", + "@dataTableColumnCalcium": { + "description": "Column header for daily percentage of calcium." + }, + "dataTableColumnIron": "Iron (%)", + "@dataTableColumnIron": { + "description": "Column header for daily percentage of iron." + }, + "dataTableRowFrozenYogurt": "Frozen yogurt", + "@dataTableRowFrozenYogurt": { + "description": "Column row for frozen yogurt." + }, + "dataTableRowIceCreamSandwich": "Ice cream sandwich", + "@dataTableRowIceCreamSandwich": { + "description": "Column row for Ice cream sandwich." + }, + "dataTableRowEclair": "Eclair", + "@dataTableRowEclair": { + "description": "Column row for Eclair." + }, + "dataTableRowCupcake": "Cupcake", + "@dataTableRowCupcake": { + "description": "Column row for Cupcake." + }, + "dataTableRowGingerbread": "Gingerbread", + "@dataTableRowGingerbread": { + "description": "Column row for Gingerbread." + }, + "dataTableRowJellyBean": "Jelly bean", + "@dataTableRowJellyBean": { + "description": "Column row for Jelly bean." + }, + "dataTableRowLollipop": "Lollipop", + "@dataTableRowLollipop": { + "description": "Column row for Lollipop." + }, + "dataTableRowHoneycomb": "Honeycomb", + "@dataTableRowHoneycomb": { + "description": "Column row for Honeycomb." + }, + "dataTableRowDonut": "Donut", + "@dataTableRowDonut": { + "description": "Column row for Donut." + }, + "dataTableRowApplePie": "Apple pie", + "@dataTableRowApplePie": { + "description": "Column row for Apple pie." + }, + "dataTableRowWithSugar": "{value} with sugar", + "@dataTableRowWithSugar": { + "description": "A dessert with sugar on it. The parameter is some type of dessert.", + "placeholders": { + "value": { + "example": "Apple pie" + } + } + }, + "dataTableRowWithHoney": "{value} with honey", + "@dataTableRowWithHoney": { + "description": "A dessert with honey on it. The parameter is some type of dessert.", + "placeholders": { + "value": { + "example": "Apple pie" + } + } + }, + "demoDialogTitle": "Dialogs", + "@demoDialogTitle": { + "description": "Title for the material dialog component demo." + }, + "demoDialogSubtitle": "Simple, alert, and fullscreen", + "@demoDialogSubtitle": { + "description": "Subtitle for the material dialog component demo." + }, + "demoAlertDialogTitle": "Alert", + "@demoAlertDialogTitle": { + "description": "Title for the alert dialog component demo." + }, + "demoAlertDialogDescription": "An alert dialog informs the user about situations that require acknowledgement. An alert dialog has an optional title and an optional list of actions.", + "@demoAlertDialogDescription": { + "description": "Description for the alert dialog component demo." + }, + "demoAlertTitleDialogTitle": "Alert With Title", + "@demoAlertTitleDialogTitle": { + "description": "Title for the alert dialog with title component demo." + }, + "demoSimpleDialogTitle": "Simple", + "@demoSimpleDialogTitle": { + "description": "Title for the simple dialog component demo." + }, + "demoSimpleDialogDescription": "A simple dialog offers the user a choice between several options. A simple dialog has an optional title that is displayed above the choices.", + "@demoSimpleDialogDescription": { + "description": "Description for the simple dialog component demo." + }, + "demoDividerTitle": "Divider", + "@demoDividerTitle": { + "description": "Title for the divider component demo." + }, + "demoDividerSubtitle": "A divider is a thin line that groups content in lists and layouts.", + "@demoDividerSubtitle": { + "description": "Subtitle for the divider component demo." + }, + "demoDividerDescription": "Dividers can be used in lists, drawers, and elsewhere to separate content.", + "@demoDividerDescription": { + "description": "Description for the divider component demo." + }, + "demoVerticalDividerTitle": "Vertical Divider", + "@demoVerticalDividerTitle": { + "description": "Title for the vertical divider component demo." + }, + "demoGridListsTitle": "Grid Lists", + "@demoGridListsTitle": { + "description": "Title for the grid lists component demo." + }, + "demoGridListsSubtitle": "Row and column layout", + "@demoGridListsSubtitle": { + "description": "Subtitle for the grid lists component demo." + }, + "demoGridListsDescription": "Grid Lists are best suited for presenting homogeneous data, typically images. Each item in a grid list is called a tile.", + "@demoGridListsDescription": { + "description": "Description for the grid lists component demo." + }, + "demoGridListsImageOnlyTitle": "Image only", + "@demoGridListsImageOnlyTitle": { + "description": "Title for the grid lists image-only component demo." + }, + "demoGridListsHeaderTitle": "With header", + "@demoGridListsHeaderTitle": { + "description": "Title for the grid lists component demo with headers on each tile." + }, + "demoGridListsFooterTitle": "With footer", + "@demoGridListsFooterTitle": { + "description": "Title for the grid lists component demo with footers on each tile." + }, + "demoSlidersTitle": "Sliders", + "@demoSlidersTitle": { + "description": "Title for the sliders component demo." + }, + "demoSlidersSubtitle": "Widgets for selecting a value by swiping", + "@demoSlidersSubtitle": { + "description": "Short description for the sliders component demo." + }, + "demoSlidersDescription": "Sliders reflect a range of values along a bar, from which users may select a single value. They are ideal for adjusting settings such as volume, brightness, or applying image filters.", + "@demoSlidersDescription": { + "description": "Description for the sliders demo." + }, + "demoRangeSlidersTitle": "Range Sliders", + "@demoRangeSlidersTitle": { + "description": "Title for the range sliders component demo." + }, + "demoRangeSlidersDescription": "Sliders reflect a range of values along a bar. They can have icons on both ends of the bar that reflect a range of values. They are ideal for adjusting settings such as volume, brightness, or applying image filters.", + "@demoRangeSlidersDescription": { + "description": "Description for the range sliders demo." + }, + "demoCustomSlidersTitle": "Custom Sliders", + "@demoCustomSlidersTitle": { + "description": "Title for the custom sliders component demo." + }, + "demoCustomSlidersDescription": "Sliders reflect a range of values along a bar, from which users may select a single value or range of values. The sliders can be themed and customized.", + "@demoCustomSlidersDescription": { + "description": "Description for the custom sliders demo." + }, + "demoSlidersContinuousWithEditableNumericalValue": "Continuous with Editable Numerical Value", + "@demoSlidersContinuousWithEditableNumericalValue": { + "description": "Text to describe a slider has a continuous value with an editable numerical value." + }, + "demoSlidersDiscrete": "Discrete", + "@demoSlidersDiscrete": { + "description": "Text to describe that we have a slider with discrete values." + }, + "demoSlidersDiscreteSliderWithCustomTheme": "Discrete Slider with Custom Theme", + "@demoSlidersDiscreteSliderWithCustomTheme": { + "description": "Text to describe that we have a slider with discrete values and a custom theme. " + }, + "demoSlidersContinuousRangeSliderWithCustomTheme": "Continuous Range Slider with Custom Theme", + "@demoSlidersContinuousRangeSliderWithCustomTheme": { + "description": "Text to describe that we have a range slider with continuous values and a custom theme. " + }, + "demoSlidersContinuous": "Continuous", + "@demoSlidersContinuous": { + "description": "Text to describe that we have a slider with continuous values." + }, + "demoSlidersEditableNumericalValue": "Editable numerical value", + "@demoSlidersEditableNumericalValue": { + "description": "Label for input field that has an editable numerical value." + }, + "demoMenuTitle": "Menu", + "@demoMenuTitle": { + "description": "Title for the menu component demo." + }, + "demoContextMenuTitle": "Context menu", + "@demoContextMenuTitle": { + "description": "Title for the context menu component demo." + }, + "demoSectionedMenuTitle": "Sectioned menu", + "@demoSectionedMenuTitle": { + "description": "Title for the sectioned menu component demo." + }, + "demoSimpleMenuTitle": "Simple menu", + "@demoSimpleMenuTitle": { + "description": "Title for the simple menu component demo." + }, + "demoChecklistMenuTitle": "Checklist menu", + "@demoChecklistMenuTitle": { + "description": "Title for the checklist menu component demo." + }, + "demoMenuSubtitle": "Menu buttons and simple menus", + "@demoMenuSubtitle": { + "description": "Short description for the menu component demo." + }, + "demoMenuDescription": "A menu displays a list of choices on a temporary surface. They appear when users interact with a button, action, or other control.", + "@demoMenuDescription": { + "description": "Description for the menu demo." + }, + "demoMenuItemValueOne": "Menu item one", + "@demoMenuItemValueOne": { + "description": "The first item in a menu." + }, + "demoMenuItemValueTwo": "Menu item two", + "@demoMenuItemValueTwo": { + "description": "The second item in a menu." + }, + "demoMenuItemValueThree": "Menu item three", + "@demoMenuItemValueThree": { + "description": "The third item in a menu." + }, + "demoMenuOne": "One", + "@demoMenuOne": { + "description": "The number one." + }, + "demoMenuTwo": "Two", + "@demoMenuTwo": { + "description": "The number two." + }, + "demoMenuThree": "Three", + "@demoMenuThree": { + "description": "The number three." + }, + "demoMenuFour": "Four", + "@demoMenuFour": { + "description": "The number four." + }, + "demoMenuAnItemWithAContextMenuButton": "An item with a context menu", + "@demoMenuAnItemWithAContextMenuButton": { + "description": "Label next to a button that opens a menu. A menu displays a list of choices on a temporary surface. Used as an example in a demo." + }, + "demoMenuContextMenuItemOne": "Context menu item one", + "@demoMenuContextMenuItemOne": { + "description": "Text label for a context menu item. A menu displays a list of choices on a temporary surface. Used as an example in a demo." + }, + "demoMenuADisabledMenuItem": "Disabled menu item", + "@demoMenuADisabledMenuItem": { + "description": "Text label for a disabled menu item. A menu displays a list of choices on a temporary surface. Used as an example in a demo." + }, + "demoMenuContextMenuItemThree": "Context menu item three", + "@demoMenuContextMenuItemThree": { + "description": "Text label for a context menu item three. A menu displays a list of choices on a temporary surface. Used as an example in a demo." + }, + "demoMenuAnItemWithASectionedMenu": "An item with a sectioned menu", + "@demoMenuAnItemWithASectionedMenu": { + "description": "Label next to a button that opens a sectioned menu . A menu displays a list of choices on a temporary surface. Used as an example in a demo." + }, + "demoMenuPreview": "Preview", + "@demoMenuPreview": { + "description": "Button to preview content." + }, + "demoMenuShare": "Share", + "@demoMenuShare": { + "description": "Button to share content." + }, + "demoMenuGetLink": "Get link", + "@demoMenuGetLink": { + "description": "Button to get link for content." + }, + "demoMenuRemove": "Remove", + "@demoMenuRemove": { + "description": "Button to remove content." + }, + "demoMenuSelected": "Selected: {value}", + "@demoMenuSelected": { + "description": "A text to show what value was selected.", + "placeholders": { + "value": { + "example": "1" + } + } + }, + "demoMenuChecked": "Checked: {value}", + "@demoMenuChecked": { + "description": "A text to show what value was checked.", + "placeholders": { + "value": { + "example": "1" + } + } + }, + "demoNavigationDrawerTitle": "Navigation Drawer", + "@demoNavigationDrawerTitle": { + "description": "Title for the material drawer component demo." + }, + "demoNavigationDrawerSubtitle": "Displaying a drawer within appbar", + "@demoNavigationDrawerSubtitle": { + "description": "Subtitle for the material drawer component demo." + }, + "demoNavigationDrawerDescription": "A Material Design panel that slides in horizontally from the edge of the screen to show navigation links in an application.", + "@demoNavigationDrawerDescription": { + "description": "Description for the material drawer component demo." + }, + "demoNavigationDrawerUserName": "User Name", + "@demoNavigationDrawerUserName": { + "description": "Demo username for navigation drawer." + }, + "demoNavigationDrawerUserEmail": "user.name@example.com", + "@demoNavigationDrawerUserEmail": { + "description": "Demo email for navigation drawer." + }, + "demoNavigationDrawerToPageOne": "Item One", + "@demoNavigationDrawerToPageOne": { + "description": "Drawer Item One." + }, + "demoNavigationDrawerToPageTwo": "Item Two", + "@demoNavigationDrawerToPageTwo": { + "description": "Drawer Item Two." + }, + "demoNavigationDrawerText": "Swipe from the edge or tap the upper-left icon to see the drawer", + "@demoNavigationDrawerText": { + "description": "Description to open navigation drawer." + }, + "demoNavigationRailTitle": "Navigation Rail", + "@demoNavigationRailTitle": { + "description": "Title for the material Navigation Rail component demo." + }, + "demoNavigationRailSubtitle": "Displaying a Navigation Rail within an app", + "@demoNavigationRailSubtitle": { + "description": "Subtitle for the material Navigation Rail component demo." + }, + "demoNavigationRailDescription": "A material widget that is meant to be displayed at the left or right of an app to navigate between a small number of views, typically between three and five.", + "@demoNavigationRailDescription": { + "description": "Description for the material Navigation Rail component demo." + }, + "demoNavigationRailFirst": "First", + "@demoNavigationRailFirst": { + "description": "Navigation Rail destination first label." + }, + "demoNavigationRailSecond": "Second", + "@demoNavigationRailSecond": { + "description": "Navigation Rail destination second label." + }, + "demoNavigationRailThird": "Third", + "@demoNavigationRailThird": { + "description": "Navigation Rail destination Third label." + }, + "demoMenuAnItemWithASimpleMenu": "An item with a simple menu", + "@demoMenuAnItemWithASimpleMenu": { + "description": "Label next to a button that opens a simple menu. A menu displays a list of choices on a temporary surface. Used as an example in a demo." + }, + "demoMenuAnItemWithAChecklistMenu": "An item with a checklist menu", + "@demoMenuAnItemWithAChecklistMenu": { + "description": "Label next to a button that opens a checklist menu. A menu displays a list of choices on a temporary surface. Used as an example in a demo." + }, + "demoFullscreenDialogTitle": "Fullscreen", + "@demoFullscreenDialogTitle": { + "description": "Title for the fullscreen dialog component demo." + }, + "demoFullscreenDialogDescription": "The fullscreenDialog property specifies whether the incoming page is a fullscreen modal dialog", + "@demoFullscreenDialogDescription": { + "description": "Description for the fullscreen dialog component demo." + }, + "demoCupertinoActivityIndicatorTitle": "Activity indicator", + "@demoCupertinoActivityIndicatorTitle": { + "description": "Title for the cupertino activity indicator component demo." + }, + "demoCupertinoActivityIndicatorSubtitle": "iOS-style activity indicators", + "@demoCupertinoActivityIndicatorSubtitle": { + "description": "Subtitle for the cupertino activity indicator component demo." + }, + "demoCupertinoActivityIndicatorDescription": "An iOS-style activity indicator that spins clockwise.", + "@demoCupertinoActivityIndicatorDescription": { + "description": "Description for the cupertino activity indicator component demo." + }, + "demoCupertinoButtonsTitle": "Buttons", + "@demoCupertinoButtonsTitle": { + "description": "Title for the cupertino buttons component demo." + }, + "demoCupertinoButtonsSubtitle": "iOS-style buttons", + "@demoCupertinoButtonsSubtitle": { + "description": "Subtitle for the cupertino buttons component demo." + }, + "demoCupertinoButtonsDescription": "An iOS-style button. It takes in text and/or an icon that fades out and in on touch. May optionally have a background.", + "@demoCupertinoButtonsDescription": { + "description": "Description for the cupertino buttons component demo." + }, + "demoCupertinoContextMenuTitle": "Context Menu", + "@demoCupertinoContextMenuTitle": { + "description": "Title for the cupertino context menu component demo." + }, + "demoCupertinoContextMenuSubtitle": "iOS-style context menu", + "@demoCupertinoContextMenuSubtitle": { + "description": "Subtitle for the cupertino context menu component demo." + }, + "demoCupertinoContextMenuDescription": "An iOS-style full screen contextual menu that appears when an element is long-pressed.", + "@demoCupertinoContextMenuDescription": { + "description": "Description for the cupertino context menu component demo." + }, + "demoCupertinoContextMenuActionOne": "Action one", + "@demoCupertinoContextMenuActionOne": { + "description": "Context menu list item one" + }, + "demoCupertinoContextMenuActionTwo": "Action two", + "@demoCupertinoContextMenuActionTwo": { + "description": "Context menu list item two" + }, + "demoCupertinoContextMenuActionText": "Tap and hold the Flutter logo to see the context menu.", + "@demoCupertinoContextMenuActionText": { + "description": "Context menu text." + }, + "demoCupertinoAlertsTitle": "Alerts", + "@demoCupertinoAlertsTitle": { + "description": "Title for the cupertino alerts component demo." + }, + "demoCupertinoAlertsSubtitle": "iOS-style alert dialogs", + "@demoCupertinoAlertsSubtitle": { + "description": "Subtitle for the cupertino alerts component demo." + }, + "demoCupertinoAlertTitle": "Alert", + "@demoCupertinoAlertTitle": { + "description": "Title for the cupertino alert component demo." + }, + "demoCupertinoAlertDescription": "An alert dialog informs the user about situations that require acknowledgement. An alert dialog has an optional title, optional content, and an optional list of actions. The title is displayed above the content and the actions are displayed below the content.", + "@demoCupertinoAlertDescription": { + "description": "Description for the cupertino alert component demo." + }, + "demoCupertinoAlertWithTitleTitle": "Alert With Title", + "@demoCupertinoAlertWithTitleTitle": { + "description": "Title for the cupertino alert with title component demo." + }, + "demoCupertinoAlertButtonsTitle": "Alert With Buttons", + "@demoCupertinoAlertButtonsTitle": { + "description": "Title for the cupertino alert with buttons component demo." + }, + "demoCupertinoAlertButtonsOnlyTitle": "Alert Buttons Only", + "@demoCupertinoAlertButtonsOnlyTitle": { + "description": "Title for the cupertino alert buttons only component demo." + }, + "demoCupertinoActionSheetTitle": "Action Sheet", + "@demoCupertinoActionSheetTitle": { + "description": "Title for the cupertino action sheet component demo." + }, + "demoCupertinoActionSheetDescription": "An action sheet is a specific style of alert that presents the user with a set of two or more choices related to the current context. An action sheet can have a title, an additional message, and a list of actions.", + "@demoCupertinoActionSheetDescription": { + "description": "Description for the cupertino action sheet component demo." + }, + "demoCupertinoNavigationBarTitle": "Navigation bar", + "@demoCupertinoNavigationBarTitle": { + "description": "Title for the cupertino navigation bar component demo." + }, + "demoCupertinoNavigationBarSubtitle": "iOS-style navigation bar", + "@demoCupertinoNavigationBarSubtitle": { + "description": "Subtitle for the cupertino navigation bar component demo." + }, + "demoCupertinoNavigationBarDescription": "An iOS-styled navigation bar. The navigation bar is a toolbar that minimally consists of a page title, in the middle of the toolbar.", + "@demoCupertinoNavigationBarDescription": { + "description": "Description for the cupertino navigation bar component demo." + }, + "demoCupertinoPickerTitle": "Pickers", + "@demoCupertinoPickerTitle": { + "description": "Title for the cupertino pickers component demo." + }, + "demoCupertinoPickerSubtitle": "iOS-style pickers", + "@demoCupertinoPickerSubtitle": { + "description": "Subtitle for the cupertino pickers component demo." + }, + "demoCupertinoPickerDescription": "An iOS-style picker widget that can be used to select strings, dates, times, or both date and time.", + "@demoCupertinoPickerDescription": { + "description": "Description for the cupertino pickers component demo." + }, + "demoCupertinoPickerTimer": "Timer", + "@demoCupertinoPickerTimer": { + "description": "Label to open a countdown timer picker." + }, + "demoCupertinoPicker": "Picker", + "@demoCupertinoPicker": { + "description": "Label to open an iOS picker." + }, + "demoCupertinoPickerDate": "Date", + "@demoCupertinoPickerDate": { + "description": "Label to open a date picker." + }, + "demoCupertinoPickerTime": "Time", + "@demoCupertinoPickerTime": { + "description": "Label to open a time picker." + }, + "demoCupertinoPickerDateTime": "Date and Time", + "@demoCupertinoPickerDateTime": { + "description": "Label to open a date and time picker." + }, + "demoCupertinoPullToRefreshTitle": "Pull to refresh", + "@demoCupertinoPullToRefreshTitle": { + "description": "Title for the cupertino pull-to-refresh component demo." + }, + "demoCupertinoPullToRefreshSubtitle": "iOS-style pull to refresh control", + "@demoCupertinoPullToRefreshSubtitle": { + "description": "Subtitle for the cupertino pull-to-refresh component demo." + }, + "demoCupertinoPullToRefreshDescription": "A widget implementing the iOS-style pull to refresh content control.", + "@demoCupertinoPullToRefreshDescription": { + "description": "Description for the cupertino pull-to-refresh component demo." + }, + "demoCupertinoSegmentedControlTitle": "Segmented control", + "@demoCupertinoSegmentedControlTitle": { + "description": "Title for the cupertino segmented control component demo." + }, + "demoCupertinoSegmentedControlSubtitle": "iOS-style segmented control", + "@demoCupertinoSegmentedControlSubtitle": { + "description": "Subtitle for the cupertino segmented control component demo." + }, + "demoCupertinoSegmentedControlDescription": "Used to select between a number of mutually exclusive options. When one option in the segmented control is selected, the other options in the segmented control cease to be selected.", + "@demoCupertinoSegmentedControlDescription": { + "description": "Description for the cupertino segmented control component demo." + }, + "demoCupertinoSliderTitle": "Slider", + "@demoCupertinoSliderTitle": { + "description": "Title for the cupertino slider component demo." + }, + "demoCupertinoSliderSubtitle": "iOS-style slider", + "@demoCupertinoSliderSubtitle": { + "description": "Subtitle for the cupertino slider component demo." + }, + "demoCupertinoSliderDescription": "A slider can be used to select from either a continuous or a discrete set of values.", + "@demoCupertinoSliderDescription": { + "description": "Description for the cupertino slider component demo." + }, + "demoCupertinoSliderContinuous": "Continuous: {value}", + "@demoCupertinoSliderContinuous": { + "description": "A label for a continuous slider that indicates what value it is set to.", + "placeholders": { + "value": { + "example": "1" + } + } + }, + "demoCupertinoSliderDiscrete": "Discrete: {value}", + "@demoCupertinoSliderDiscrete": { + "description": "A label for a discrete slider that indicates what value it is set to.", + "placeholders": { + "value": { + "example": "1" + } + } + }, + "demoCupertinoSwitchSubtitle": "iOS-style switch", + "@demoCupertinoSwitchSubtitle": { + "description": "Subtitle for the cupertino switch component demo." + }, + "demoCupertinoSwitchDescription": "A switch is used to toggle the on/off state of a single setting.", + "@demoCupertinoSwitchDescription": { + "description": "Description for the cupertino switch component demo." + }, + "demoCupertinoTabBarTitle": "Tab bar", + "@demoCupertinoTabBarTitle": { + "description": "Title for the cupertino bottom tab bar demo." + }, + "demoCupertinoTabBarSubtitle": "iOS-style bottom tab bar", + "@demoCupertinoTabBarSubtitle": { + "description": "Subtitle for the cupertino bottom tab bar demo." + }, + "demoCupertinoTabBarDescription": "An iOS-style bottom navigation tab bar. Displays multiple tabs with one tab being active, the first tab by default.", + "@demoCupertinoTabBarDescription": { + "description": "Description for the cupertino bottom tab bar demo." + }, + "cupertinoTabBarHomeTab": "Home", + "@cupertinoTabBarHomeTab": { + "description": "Title for the home tab in the bottom tab bar demo." + }, + "cupertinoTabBarChatTab": "Chat", + "@cupertinoTabBarChatTab": { + "description": "Title for the chat tab in the bottom tab bar demo." + }, + "cupertinoTabBarProfileTab": "Profile", + "@cupertinoTabBarProfileTab": { + "description": "Title for the profile tab in the bottom tab bar demo." + }, + "demoCupertinoTextFieldTitle": "Text fields", + "@demoCupertinoTextFieldTitle": { + "description": "Title for the cupertino text field demo." + }, + "demoCupertinoTextFieldSubtitle": "iOS-style text fields", + "@demoCupertinoTextFieldSubtitle": { + "description": "Subtitle for the cupertino text field demo." + }, + "demoCupertinoTextFieldDescription": "A text field lets the user enter text, either with a hardware keyboard or with an onscreen keyboard.", + "@demoCupertinoTextFieldDescription": { + "description": "Description for the cupertino text field demo." + }, + "demoCupertinoTextFieldPIN": "PIN", + "@demoCupertinoTextFieldPIN": { + "description": "The placeholder for a text field where a user would enter their PIN number." + }, + "demoCupertinoSearchTextFieldTitle": "Search text field", + "@demoCupertinoSearchTextFieldTitle": { + "description": "Title for the cupertino search text field demo." + }, + "demoCupertinoSearchTextFieldSubtitle": "iOS-style search text field", + "@demoCupertinoSearchTextFieldSubtitle": { + "description": "Subtitle for the cupertino search text field demo." + }, + "demoCupertinoSearchTextFieldDescription": "A search text field that lets the user search by entering text, and that can offer and filter suggestions.", + "@demoCupertinoSearchTextFieldDescription": { + "description": "Description for the cupertino search text field demo." + }, + "demoCupertinoSearchTextFieldPlaceholder": "Enter some text", + "@demoCupertinoSearchTextFieldPlaceholder": { + "description": "The placeholder for a search text field demo." + }, + "demoCupertinoScrollbarTitle": "Scrollbar", + "@demoCupertinoScrollbarTitle": { + "description": "Title for the cupertino scrollbar demo." + }, + "demoCupertinoScrollbarSubtitle": "iOS-style scrollbar", + "@demoCupertinoScrollbarSubtitle": { + "description": "Subtitle for the cupertino scrollbar demo." + }, + "demoCupertinoScrollbarDescription": "A scrollbar that wraps the given child", + "@demoCupertinoScrollbarDescription": { + "description": "Description for the cupertino scrollbar demo." + }, + "demoMotionTitle": "Motion", + "@demoMotionTitle": { + "description": "Title for the motion demo." + }, + "demoMotionSubtitle": "All of the predefined transition patterns", + "@demoMotionSubtitle": { + "description": "Subtitle for the motion demo." + }, + "demoContainerTransformDemoInstructions": "Cards, Lists & FAB", + "@demoContainerTransformDemoInstructions": { + "description": "Instructions for the container transform demo located in the app bar." + }, + "demoSharedXAxisDemoInstructions": "Next and Back Buttons", + "@demoSharedXAxisDemoInstructions": { + "description": "Instructions for the shared x axis demo located in the app bar." + }, + "demoSharedYAxisDemoInstructions": "Sort by \"Recently Played\"", + "@demoSharedYAxisDemoInstructions": { + "description": "Instructions for the shared y axis demo located in the app bar." + }, + "demoSharedZAxisDemoInstructions": "Settings icon button", + "@demoSharedZAxisDemoInstructions": { + "description": "Instructions for the shared z axis demo located in the app bar." + }, + "demoFadeThroughDemoInstructions": "Bottom navigation", + "@demoFadeThroughDemoInstructions": { + "description": "Instructions for the fade through demo located in the app bar." + }, + "demoFadeScaleDemoInstructions": "Modal and FAB", + "@demoFadeScaleDemoInstructions": { + "description": "Instructions for the fade scale demo located in the app bar." + }, + "demoContainerTransformTitle": "Container Transform", + "@demoContainerTransformTitle": { + "description": "Title for the container transform demo." + }, + "demoContainerTransformDescription": "The container transform pattern is designed for transitions between UI elements that include a container. This pattern creates a visible connection between two UI elements", + "@demoContainerTransformDescription": { + "description": "Description for the container transform demo." + }, + "demoContainerTransformModalBottomSheetTitle": "Fade mode", + "@demoContainerTransformModalBottomSheetTitle": { + "description": "Title for the container transform modal bottom sheet." + }, + "demoContainerTransformTypeFade": "FADE", + "@demoContainerTransformTypeFade": { + "description": "Description for container transform fade type setting." + }, + "demoContainerTransformTypeFadeThrough": "FADE THROUGH", + "@demoContainerTransformTypeFadeThrough": { + "description": "Description for container transform fade through type setting." + }, + "demoMotionPlaceholderTitle": "Title", + "@demoMotionPlaceholderTitle": { + "description": "The placeholder for the motion demos title properties." + }, + "demoMotionPlaceholderSubtitle": "Secondary text", + "@demoMotionPlaceholderSubtitle": { + "description": "The placeholder for the motion demos subtitle properties." + }, + "demoMotionSmallPlaceholderSubtitle": "Secondary", + "@demoMotionSmallPlaceholderSubtitle": { + "description": "The placeholder for the motion demos shortened subtitle properties." + }, + "demoMotionDetailsPageTitle": "Details Page", + "@demoMotionDetailsPageTitle": { + "description": "The title for the details page in the motion demos." + }, + "demoMotionListTileTitle": "List item", + "@demoMotionListTileTitle": { + "description": "The title for a list tile in the motion demos." + }, + "demoSharedAxisDescription": "The shared axis pattern is used for transitions between the UI elements that have a spatial or navigational relationship. This pattern uses a shared transformation on the x, y, or z axis to reinforce the relationship between elements.", + "@demoSharedAxisDescription": { + "description": "Description for the shared y axis demo." + }, + "demoSharedXAxisTitle": "Shared x-axis", + "@demoSharedXAxisTitle": { + "description": "Title for the shared x axis demo." + }, + "demoSharedXAxisBackButtonText": "BACK", + "@demoSharedXAxisBackButtonText": { + "description": "Button text for back button in the shared x axis demo." + }, + "demoSharedXAxisNextButtonText": "NEXT", + "@demoSharedXAxisNextButtonText": { + "description": "Button text for the next button in the shared x axis demo." + }, + "demoSharedXAxisCoursePageTitle": "Streamline your courses", + "@demoSharedXAxisCoursePageTitle": { + "description": "Title for course selection page in the shared x axis demo." + }, + "demoSharedXAxisCoursePageSubtitle": "Bundled categories appear as groups in your feed. You can always change this later.", + "@demoSharedXAxisCoursePageSubtitle": { + "description": "Subtitle for course selection page in the shared x axis demo." + }, + "demoSharedXAxisArtsAndCraftsCourseTitle": "Arts & Crafts", + "@demoSharedXAxisArtsAndCraftsCourseTitle": { + "description": "Title for the Arts & Crafts course in the shared x axis demo." + }, + "demoSharedXAxisBusinessCourseTitle": "Business", + "@demoSharedXAxisBusinessCourseTitle": { + "description": "Title for the Business course in the shared x axis demo." + }, + "demoSharedXAxisIllustrationCourseTitle": "Illustration", + "@demoSharedXAxisIllustrationCourseTitle": { + "description": "Title for the Illustration course in the shared x axis demo." + }, + "demoSharedXAxisDesignCourseTitle": "Design", + "@demoSharedXAxisDesignCourseTitle": { + "description": "Title for the Design course in the shared x axis demo." + }, + "demoSharedXAxisCulinaryCourseTitle": "Culinary", + "@demoSharedXAxisCulinaryCourseTitle": { + "description": "Title for the Culinary course in the shared x axis demo." + }, + "demoSharedXAxisBundledCourseSubtitle": "Bundled", + "@demoSharedXAxisBundledCourseSubtitle": { + "description": "Subtitle for a bundled course in the shared x axis demo." + }, + "demoSharedXAxisIndividualCourseSubtitle": "Shown Individually", + "@demoSharedXAxisIndividualCourseSubtitle": { + "description": "Subtitle for a individual course in the shared x axis demo." + }, + "demoSharedXAxisSignInWelcomeText": "Hi David Park", + "@demoSharedXAxisSignInWelcomeText": { + "description": "Welcome text for sign in page in the shared x axis demo. David Park is a name and does not need to be translated." + }, + "demoSharedXAxisSignInSubtitleText": "Sign in with your account", + "@demoSharedXAxisSignInSubtitleText": { + "description": "Subtitle text for sign in page in the shared x axis demo." + }, + "demoSharedXAxisSignInTextFieldLabel": "Email or phone number", + "@demoSharedXAxisSignInTextFieldLabel": { + "description": "Label text for the sign in text field in the shared x axis demo." + }, + "demoSharedXAxisForgotEmailButtonText": "FORGOT EMAIL?", + "@demoSharedXAxisForgotEmailButtonText": { + "description": "Button text for the forgot email button in the shared x axis demo." + }, + "demoSharedXAxisCreateAccountButtonText": "CREATE ACCOUNT", + "@demoSharedXAxisCreateAccountButtonText": { + "description": "Button text for the create account button in the shared x axis demo." + }, + "demoSharedYAxisTitle": "Shared y-axis", + "@demoSharedYAxisTitle": { + "description": "Title for the shared y axis demo." + }, + "demoSharedYAxisAlbumCount": "268 albums", + "@demoSharedYAxisAlbumCount": { + "description": "Text for album count in the shared y axis demo." + }, + "demoSharedYAxisAlphabeticalSortTitle": "A-Z", + "@demoSharedYAxisAlphabeticalSortTitle": { + "description": "Title for alphabetical sorting type in the shared y axis demo." + }, + "demoSharedYAxisRecentSortTitle": "Recently played", + "@demoSharedYAxisRecentSortTitle": { + "description": "Title for recently played sorting type in the shared y axis demo." + }, + "demoSharedYAxisAlbumTileTitle": "Album", + "@demoSharedYAxisAlbumTileTitle": { + "description": "Title for an AlbumTile in the shared y axis demo." + }, + "demoSharedYAxisAlbumTileSubtitle": "Artist", + "@demoSharedYAxisAlbumTileSubtitle": { + "description": "Subtitle for an AlbumTile in the shared y axis demo." + }, + "demoSharedYAxisAlbumTileDurationUnit": "min", + "@demoSharedYAxisAlbumTileDurationUnit": { + "description": "Duration unit for an AlbumTile in the shared y axis demo." + }, + "demoSharedZAxisTitle": "Shared z-axis", + "@demoSharedZAxisTitle": { + "description": "Title for the shared z axis demo." + }, + "demoSharedZAxisSettingsPageTitle": "Settings", + "@demoSharedZAxisSettingsPageTitle": { + "description": "Title for the settings page in the shared z axis demo." + }, + "demoSharedZAxisBurgerRecipeTitle": "Burger", + "@demoSharedZAxisBurgerRecipeTitle": { + "description": "Title for burger recipe tile in the shared z axis demo." + }, + "demoSharedZAxisBurgerRecipeDescription": "Burger recipe", + "@demoSharedZAxisBurgerRecipeDescription": { + "description": "Subtitle for the burger recipe tile in the shared z axis demo." + }, + "demoSharedZAxisSandwichRecipeTitle": "Sandwich", + "@demoSharedZAxisSandwichRecipeTitle": { + "description": "Title for sandwich recipe tile in the shared z axis demo." + }, + "demoSharedZAxisSandwichRecipeDescription": "Sandwich recipe", + "@demoSharedZAxisSandwichRecipeDescription": { + "description": "Subtitle for the sandwich recipe tile in the shared z axis demo." + }, + "demoSharedZAxisDessertRecipeTitle": "Dessert", + "@demoSharedZAxisDessertRecipeTitle": { + "description": "Title for dessert recipe tile in the shared z axis demo." + }, + "demoSharedZAxisDessertRecipeDescription": "Dessert recipe", + "@demoSharedZAxisDessertRecipeDescription": { + "description": "Subtitle for the dessert recipe tile in the shared z axis demo." + }, + "demoSharedZAxisShrimpPlateRecipeTitle": "Shrimp", + "@demoSharedZAxisShrimpPlateRecipeTitle": { + "description": "Title for shrimp plate recipe tile in the shared z axis demo." + }, + "demoSharedZAxisShrimpPlateRecipeDescription": "Shrimp plate recipe", + "@demoSharedZAxisShrimpPlateRecipeDescription": { + "description": "Subtitle for the shrimp plate recipe tile in the shared z axis demo." + }, + "demoSharedZAxisCrabPlateRecipeTitle": "Crab", + "@demoSharedZAxisCrabPlateRecipeTitle": { + "description": "Title for crab plate recipe tile in the shared z axis demo." + }, + "demoSharedZAxisCrabPlateRecipeDescription": "Crab plate recipe", + "@demoSharedZAxisCrabPlateRecipeDescription": { + "description": "Subtitle for the crab plate recipe tile in the shared z axis demo." + }, + "demoSharedZAxisBeefSandwichRecipeTitle": "Beef Sandwich", + "@demoSharedZAxisBeefSandwichRecipeTitle": { + "description": "Title for beef sandwich recipe tile in the shared z axis demo." + }, + "demoSharedZAxisBeefSandwichRecipeDescription": "Beef Sandwich recipe", + "@demoSharedZAxisBeefSandwichRecipeDescription": { + "description": "Subtitle for the beef sandwich recipe tile in the shared z axis demo." + }, + "demoSharedZAxisSavedRecipesListTitle": "Saved Recipes", + "@demoSharedZAxisSavedRecipesListTitle": { + "description": "Title for list of saved recipes in the shared z axis demo." + }, + "demoSharedZAxisProfileSettingLabel": "Profile", + "@demoSharedZAxisProfileSettingLabel": { + "description": "Text label for profile setting tile in the shared z axis demo." + }, + "demoSharedZAxisNotificationSettingLabel": "Notifications", + "@demoSharedZAxisNotificationSettingLabel": { + "description": "Text label for notifications setting tile in the shared z axis demo." + }, + "demoSharedZAxisPrivacySettingLabel": "Privacy", + "@demoSharedZAxisPrivacySettingLabel": { + "description": "Text label for the privacy setting tile in the shared z axis demo." + }, + "demoSharedZAxisHelpSettingLabel": "Help", + "@demoSharedZAxisHelpSettingLabel": { + "description": "Text label for the help setting tile in the shared z axis demo." + }, + "demoFadeThroughTitle": "Fade through", + "@demoFadeThroughTitle": { + "description": "Title for the fade through demo." + }, + "demoFadeThroughDescription": "The fade through pattern is used for transitions between UI elements that do not have a strong relationship to each other.", + "@demoFadeThroughDescription": { + "description": "Description for the fade through demo." + }, + "demoFadeThroughAlbumsDestination": "Albums", + "@demoFadeThroughAlbumsDestination": { + "description": "Text for albums bottom navigation bar destination in the fade through demo." + }, + "demoFadeThroughPhotosDestination": "Photos", + "@demoFadeThroughPhotosDestination": { + "description": "Text for photos bottom navigation bar destination in the fade through demo." + }, + "demoFadeThroughSearchDestination": "Search", + "@demoFadeThroughSearchDestination": { + "description": "Text for search bottom navigation bar destination in the fade through demo." + }, + "demoFadeThroughTextPlaceholder": "123 photos", + "@demoFadeThroughTextPlaceholder": { + "description": "Placeholder for example card title in the fade through demo." + }, + "demoFadeScaleTitle": "Fade", + "@demoFadeScaleTitle": { + "description": "Title for the fade scale demo." + }, + "demoFadeScaleDescription": "The fade pattern is used for UI elements that enter or exit within the bounds of the screen, such as a dialog that fades in the center of the screen.", + "@demoFadeScaleDescription": { + "description": "Description for the fade scale demo." + }, + "demoFadeScaleShowAlertDialogButton": "SHOW MODAL", + "@demoFadeScaleShowAlertDialogButton": { + "description": "Button text to show alert dialog in the fade scale demo." + }, + "demoFadeScaleShowFabButton": "SHOW FAB", + "@demoFadeScaleShowFabButton": { + "description": "Button text to show fab in the fade scale demo." + }, + "demoFadeScaleHideFabButton": "HIDE FAB", + "@demoFadeScaleHideFabButton": { + "description": "Button text to hide fab in the fade scale demo." + }, + "demoFadeScaleAlertDialogHeader": "Alert Dialog", + "@demoFadeScaleAlertDialogHeader": { + "description": "Generic header for alert dialog in the fade scale demo." + }, + "demoFadeScaleAlertDialogCancelButton": "CANCEL", + "@demoFadeScaleAlertDialogCancelButton": { + "description": "Button text for alert dialog cancel button in the fade scale demo." + }, + "demoFadeScaleAlertDialogDiscardButton": "DISCARD", + "@demoFadeScaleAlertDialogDiscardButton": { + "description": "Button text for alert dialog discard button in the fade scale demo." + }, + "demoColorsTitle": "Colors", + "@demoColorsTitle": { + "description": "Title for the colors demo." + }, + "demoColorsSubtitle": "All of the predefined colors", + "@demoColorsSubtitle": { + "description": "Subtitle for the colors demo." + }, + "demoColorsDescription": "Color and color swatch constants which represent Material Design's color palette.", + "@demoColorsDescription": { + "description": "Description for the colors demo. Material Design should remain capitalized." + }, + "demoTypographyTitle": "Typography", + "@demoTypographyTitle": { + "description": "Title for the typography demo." + }, + "demoTypographySubtitle": "All of the predefined text styles", + "@demoTypographySubtitle": { + "description": "Subtitle for the typography demo." + }, + "demoTypographyDescription": "Definitions for the various typographical styles found in Material Design.", + "@demoTypographyDescription": { + "description": "Description for the typography demo. Material Design should remain capitalized." + }, + "demo2dTransformationsTitle": "2D transformations", + "@demo2dTransformationsTitle": { + "description": "Title for the 2D transformations demo." + }, + "demo2dTransformationsSubtitle": "Pan and zoom", + "@demo2dTransformationsSubtitle": { + "description": "Subtitle for the 2D transformations demo." + }, + "demo2dTransformationsDescription": "Tap to edit tiles, and use gestures to move around the scene. Drag to pan and pinch with two fingers to zoom. Press the reset button to return to the starting orientation.", + "@demo2dTransformationsDescription": { + "description": "Description for the 2D transformations demo." + }, + "demo2dTransformationsResetTooltip": "Reset transformations", + "@demo2dTransformationsResetTooltip": { + "description": "Tooltip for a button to reset the transformations (scale, translation) for the 2D transformations demo." + }, + "demo2dTransformationsEditTooltip": "Edit tile", + "@demo2dTransformationsEditTooltip": { + "description": "Tooltip for a button to edit a tile." + }, + "buttonText": "BUTTON", + "@buttonText": { + "description": "Text for a generic button." + }, + "demoBottomSheetTitle": "Bottom sheet", + "@demoBottomSheetTitle": { + "description": "Title for bottom sheet demo." + }, + "demoBottomSheetSubtitle": "Persistent and modal bottom sheets", + "@demoBottomSheetSubtitle": { + "description": "Description for bottom sheet demo." + }, + "demoBottomSheetPersistentTitle": "Persistent bottom sheet", + "@demoBottomSheetPersistentTitle": { + "description": "Title for persistent bottom sheet demo." + }, + "demoBottomSheetPersistentDescription": "A persistent bottom sheet shows information that supplements the primary content of the app. A persistent bottom sheet remains visible even when the user interacts with other parts of the app.", + "@demoBottomSheetPersistentDescription": { + "description": "Description for persistent bottom sheet demo." + }, + "demoBottomSheetModalTitle": "Modal bottom sheet", + "@demoBottomSheetModalTitle": { + "description": "Title for modal bottom sheet demo." + }, + "demoBottomSheetModalDescription": "A modal bottom sheet is an alternative to a menu or a dialog and prevents the user from interacting with the rest of the app.", + "@demoBottomSheetModalDescription": { + "description": "Description for modal bottom sheet demo." + }, + "demoBottomSheetAddLabel": "Add", + "@demoBottomSheetAddLabel": { + "description": "Semantic label for add icon." + }, + "demoBottomSheetButtonText": "SHOW BOTTOM SHEET", + "@demoBottomSheetButtonText": { + "description": "Button text to show bottom sheet." + }, + "demoBottomSheetHeader": "Header", + "@demoBottomSheetHeader": { + "description": "Generic header placeholder." + }, + "demoBottomSheetItem": "Item {value}", + "@demoBottomSheetItem": { + "description": "Generic item placeholder.", + "placeholders": { + "value": { + "example": "1" + } + } + }, + "demoListsTitle": "Lists", + "@demoListsTitle": { + "description": "Title for lists demo." + }, + "demoListsSubtitle": "Scrolling list layouts", + "@demoListsSubtitle": { + "description": "Subtitle for lists demo." + }, + "demoListsDescription": "A single fixed-height row that typically contains some text as well as a leading or trailing icon.", + "@demoListsDescription": { + "description": "Description for lists demo. This describes what a single row in a list consists of." + }, + "demoOneLineListsTitle": "One Line", + "@demoOneLineListsTitle": { + "description": "Title for lists demo with only one line of text per row." + }, + "demoTwoLineListsTitle": "Two Lines", + "@demoTwoLineListsTitle": { + "description": "Title for lists demo with two lines of text per row." + }, + "demoListsSecondary": "Secondary text", + "@demoListsSecondary": { + "description": "Text that appears in the second line of a list item." + }, + "demoProgressIndicatorTitle": "Progress indicators", + "@demoProgressIndicatorTitle": { + "description": "Title for progress indicators demo." + }, + "demoProgressIndicatorSubtitle": "Linear, circular, indeterminate", + "@demoProgressIndicatorSubtitle": { + "description": "Subtitle for progress indicators demo." + }, + "demoCircularProgressIndicatorTitle": "Circular Progress Indicator", + "@demoCircularProgressIndicatorTitle": { + "description": "Title for circular progress indicator demo." + }, + "demoCircularProgressIndicatorDescription": "A Material Design circular progress indicator, which spins to indicate that the application is busy.", + "@demoCircularProgressIndicatorDescription": { + "description": "Description for circular progress indicator demo." + }, + "demoLinearProgressIndicatorTitle": "Linear Progress Indicator", + "@demoLinearProgressIndicatorTitle": { + "description": "Title for linear progress indicator demo." + }, + "demoLinearProgressIndicatorDescription": "A Material Design linear progress indicator, also known as a progress bar.", + "@demoLinearProgressIndicatorDescription": { + "description": "Description for linear progress indicator demo." + }, + "demoPickersTitle": "Pickers", + "@demoPickersTitle": { + "description": "Title for pickers demo." + }, + "demoPickersSubtitle": "Date and time selection", + "@demoPickersSubtitle": { + "description": "Subtitle for pickers demo." + }, + "demoDatePickerTitle": "Date Picker", + "@demoDatePickerTitle": { + "description": "Title for date picker demo." + }, + "demoDatePickerDescription": "Shows a dialog containing a Material Design date picker.", + "@demoDatePickerDescription": { + "description": "Description for date picker demo." + }, + "demoTimePickerTitle": "Time Picker", + "@demoTimePickerTitle": { + "description": "Title for time picker demo." + }, + "demoTimePickerDescription": "Shows a dialog containing a Material Design time picker.", + "@demoTimePickerDescription": { + "description": "Description for time picker demo." + }, + "demoDateRangePickerTitle": "Date Range Picker", + "@demoDateRangePickerTitle": { + "description": "Title for date range picker demo." + }, + "demoDateRangePickerDescription": "Shows a dialog containing a Material Design date range picker.", + "@demoDateRangePickerDescription": { + "description": "Description for date range picker demo." + }, + "demoPickersShowPicker": "SHOW PICKER", + "@demoPickersShowPicker": { + "description": "Button text to show the date or time picker in the demo." + }, + "demoTabsTitle": "Tabs", + "@demoTabsTitle": { + "description": "Title for tabs demo." + }, + "demoTabsScrollingTitle": "Scrolling", + "@demoTabsScrollingTitle": { + "description": "Title for tabs demo with a tab bar that scrolls." + }, + "demoTabsNonScrollingTitle": "Non-scrolling", + "@demoTabsNonScrollingTitle": { + "description": "Title for tabs demo with a tab bar that doesn't scroll." + }, + "demoTabsSubtitle": "Tabs with independently scrollable views", + "@demoTabsSubtitle": { + "description": "Subtitle for tabs demo." + }, + "demoTabsDescription": "Tabs organize content across different screens, data sets, and other interactions.", + "@demoTabsDescription": { + "description": "Description for tabs demo." + }, + "demoSnackbarsTitle": "Snackbars", + "@demoSnackbarsTitle": { + "description": "Title for snackbars demo." + }, + "demoSnackbarsSubtitle": "Snackbars show messages at the bottom of the screen", + "@demoSnackbarsSubtitle": { + "description": "Subtitle for snackbars demo." + }, + "demoSnackbarsDescription": "Snackbars inform users of a process that an app has performed or will perform. They appear temporarily, towards the bottom of the screen. They shouldn't interrupt the user experience, and they don't require user input to disappear.", + "@demoSnackbarsDescription": { + "description": "Description for snackbars demo." + }, + "demoSnackbarsButtonLabel": "SHOW A SNACKBAR", + "@demoSnackbarsButtonLabel": { + "description": "Label for button to show a snackbar." + }, + "demoSnackbarsText": "This is a snackbar.", + "@demoSnackbarsText": { + "description": "Text to show on a snackbar." + }, + "demoSnackbarsActionButtonLabel": "ACTION", + "@demoSnackbarsActionButtonLabel": { + "description": "Label for action button text on the snackbar." + }, + "demoSnackbarsAction": "You pressed the snackbar action.", + "@demoSnackbarsAction": { + "description": "Text that appears when you press on a snackbars' action." + }, + "demoSelectionControlsTitle": "Selection controls", + "@demoSelectionControlsTitle": { + "description": "Title for selection controls demo." + }, + "demoSelectionControlsSubtitle": "Checkboxes, radio buttons, and switches", + "@demoSelectionControlsSubtitle": { + "description": "Subtitle for selection controls demo." + }, + "demoSelectionControlsCheckboxTitle": "Checkbox", + "@demoSelectionControlsCheckboxTitle": { + "description": "Title for the checkbox (selection controls) demo." + }, + "demoSelectionControlsCheckboxDescription": "Checkboxes allow the user to select multiple options from a set. A normal checkbox's value is true or false and a tristate checkbox's value can also be null.", + "@demoSelectionControlsCheckboxDescription": { + "description": "Description for the checkbox (selection controls) demo." + }, + "demoSelectionControlsRadioTitle": "Radio", + "@demoSelectionControlsRadioTitle": { + "description": "Title for the radio button (selection controls) demo." + }, + "demoSelectionControlsRadioDescription": "Radio buttons allow the user to select one option from a set. Use radio buttons for exclusive selection if you think that the user needs to see all available options side-by-side.", + "@demoSelectionControlsRadioDescription": { + "description": "Description for the radio button (selection controls) demo." + }, + "demoSelectionControlsSwitchTitle": "Switch", + "@demoSelectionControlsSwitchTitle": { + "description": "Title for the switches (selection controls) demo." + }, + "demoSelectionControlsSwitchDescription": "On/off switches toggle the state of a single settings option. The option that the switch controls, as well as the state it's in, should be made clear from the corresponding inline label.", + "@demoSelectionControlsSwitchDescription": { + "description": "Description for the switches (selection controls) demo." + }, + "demoBottomTextFieldsTitle": "Text fields", + "@demoBottomTextFieldsTitle": { + "description": "Title for text fields demo." + }, + "demoTextFieldTitle": "Text fields", + "@demoTextFieldTitle": { + "description": "Title for text fields demo." + }, + "demoTextFieldSubtitle": "Single line of editable text and numbers", + "@demoTextFieldSubtitle": { + "description": "Description for text fields demo." + }, + "demoTextFieldDescription": "Text fields allow users to enter text into a UI. They typically appear in forms and dialogs.", + "@demoTextFieldDescription": { + "description": "Description for text fields demo." + }, + "demoTextFieldShowPasswordLabel": "Show password", + "@demoTextFieldShowPasswordLabel": { + "description": "Label for show password icon." + }, + "demoTextFieldHidePasswordLabel": "Hide password", + "@demoTextFieldHidePasswordLabel": { + "description": "Label for hide password icon." + }, + "demoTextFieldFormErrors": "Please fix the errors in red before submitting.", + "@demoTextFieldFormErrors": { + "description": "Text that shows up on form errors." + }, + "demoTextFieldNameRequired": "Name is required.", + "@demoTextFieldNameRequired": { + "description": "Shows up as submission error if name is not given in the form." + }, + "demoTextFieldOnlyAlphabeticalChars": "Please enter only alphabetical characters.", + "@demoTextFieldOnlyAlphabeticalChars": { + "description": "Error that shows if non-alphabetical characters are given." + }, + "demoTextFieldEnterUSPhoneNumber": "(###) ###-#### - Enter a US phone number.", + "@demoTextFieldEnterUSPhoneNumber": { + "description": "Error that shows up if non-valid non-US phone number is given." + }, + "demoTextFieldEnterPassword": "Please enter a password.", + "@demoTextFieldEnterPassword": { + "description": "Error that shows up if password is not given." + }, + "demoTextFieldPasswordsDoNotMatch": "The passwords don't match", + "@demoTextFieldPasswordsDoNotMatch": { + "description": "Error that shows up, if the re-typed password does not match the already given password." + }, + "demoTextFieldWhatDoPeopleCallYou": "What do people call you?", + "@demoTextFieldWhatDoPeopleCallYou": { + "description": "Placeholder for name field in form." + }, + "demoTextFieldNameField": "Name*", + "@demoTextFieldNameField": { + "description": "The label for a name input field that is required (hence the star)." + }, + "demoTextFieldWhereCanWeReachYou": "Where can we reach you?", + "@demoTextFieldWhereCanWeReachYou": { + "description": "Placeholder for when entering a phone number in a form." + }, + "demoTextFieldPhoneNumber": "Phone number*", + "@demoTextFieldPhoneNumber": { + "description": "The label for a phone number input field that is required (hence the star)." + }, + "demoTextFieldYourEmailAddress": "Your email address", + "@demoTextFieldYourEmailAddress": { + "description": "The label for an email address input field." + }, + "demoTextFieldEmail": "Email", + "@demoTextFieldEmail": { + "description": "The label for an email address input field" + }, + "demoTextFieldTellUsAboutYourself": "Tell us about yourself (e.g., write down what you do or what hobbies you have)", + "@demoTextFieldTellUsAboutYourself": { + "description": "The placeholder text for biography/life story input field." + }, + "demoTextFieldKeepItShort": "Keep it short, this is just a demo.", + "@demoTextFieldKeepItShort": { + "description": "Helper text for biography/life story input field." + }, + "demoTextFieldLifeStory": "Life story", + "@demoTextFieldLifeStory": { + "description": "The label for biography/life story input field." + }, + "demoTextFieldSalary": "Salary", + "@demoTextFieldSalary": { + "description": "The label for salary input field." + }, + "demoTextFieldUSD": "USD", + "@demoTextFieldUSD": { + "description": "US currency, used as suffix in input field for salary." + }, + "demoTextFieldNoMoreThan": "No more than 8 characters.", + "@demoTextFieldNoMoreThan": { + "description": "Helper text for password input field." + }, + "demoTextFieldPassword": "Password*", + "@demoTextFieldPassword": { + "description": "Label for password input field, that is required (hence the star)." + }, + "demoTextFieldRetypePassword": "Re-type password*", + "@demoTextFieldRetypePassword": { + "description": "Label for repeat password input field." + }, + "demoTextFieldSubmit": "SUBMIT", + "@demoTextFieldSubmit": { + "description": "The submit button text for form." + }, + "demoTextFieldNameHasPhoneNumber": "{name} phone number is {phoneNumber}", + "@demoTextFieldNameHasPhoneNumber": { + "description": "Text that shows up when valid phone number and name is submitted in form.", + "placeholders": { + "name": { + "example": "Peter" + }, + "phoneNumber": { + "phoneNumber": "+1 (000) 000-0000" + } + } + }, + "demoTextFieldRequiredField": "* indicates required field", + "@demoTextFieldRequiredField": { + "description": "Helper text to indicate that * means that it is a required field." + }, + "demoTooltipTitle": "Tooltips", + "@demoTooltipTitle": { + "description": "Title for tooltip demo." + }, + "demoTooltipSubtitle": "Short message displayed on long press or hover", + "@demoTooltipSubtitle": { + "description": "Subtitle for tooltip demo." + }, + "demoTooltipDescription": "Tooltips provide text labels that help explain the function of a button or other user interface action. Tooltips display informative text when users hover over, focus on, or long press an element.", + "@demoTooltipDescription": { + "description": "Description for tooltip demo." + }, + "demoTooltipInstructions": "Long press or hover to display the tooltip.", + "@demoTooltipInstructions": { + "description": "Instructions for how to trigger a tooltip in the tooltip demo." + }, + "bottomNavigationCommentsTab": "Comments", + "@bottomNavigationCommentsTab": { + "description": "Title for Comments tab of bottom navigation." + }, + "bottomNavigationCalendarTab": "Calendar", + "@bottomNavigationCalendarTab": { + "description": "Title for Calendar tab of bottom navigation." + }, + "bottomNavigationAccountTab": "Account", + "@bottomNavigationAccountTab": { + "description": "Title for Account tab of bottom navigation." + }, + "bottomNavigationAlarmTab": "Alarm", + "@bottomNavigationAlarmTab": { + "description": "Title for Alarm tab of bottom navigation." + }, + "bottomNavigationCameraTab": "Camera", + "@bottomNavigationCameraTab": { + "description": "Title for Camera tab of bottom navigation." + }, + "bottomNavigationContentPlaceholder": "Placeholder for {title} tab", + "@bottomNavigationContentPlaceholder": { + "description": "Accessibility label for the content placeholder in the bottom navigation demo", + "placeholders": { + "title": { + "example": "Account" + } + } + }, + "buttonTextCreate": "Create", + "@buttonTextCreate": { + "description": "Tooltip text for a create button." + }, + "dialogSelectedOption": "You selected: \"{value}\"", + "@dialogSelectedOption": { + "description": "Message displayed after an option is selected from a dialog", + "placeholders": { + "value": { + "example": "AGREE" + } + } + }, + "chipTurnOnLights": "Turn on lights", + "@chipTurnOnLights": { + "description": "A chip component to turn on the lights." + }, + "chipSmall": "Small", + "@chipSmall": { + "description": "A chip component to select a small size." + }, + "chipMedium": "Medium", + "@chipMedium": { + "description": "A chip component to select a medium size." + }, + "chipLarge": "Large", + "@chipLarge": { + "description": "A chip component to select a large size." + }, + "chipElevator": "Elevator", + "@chipElevator": { + "description": "A chip component to filter selection by elevators." + }, + "chipWasher": "Washer", + "@chipWasher": { + "description": "A chip component to filter selection by washers." + }, + "chipFireplace": "Fireplace", + "@chipFireplace": { + "description": "A chip component to filter selection by fireplaces." + }, + "chipBiking": "Biking", + "@chipBiking": { + "description": "A chip component to that indicates a biking selection." + }, + "demo": "Demo", + "@demo": { + "description": "Used in the title of the demos." + }, + "bottomAppBar": "Bottom app bar", + "@bottomAppBar": { + "description": "Used as semantic label for a BottomAppBar." + }, + "loading": "Loading", + "@loading": { + "description": "Indicates the loading process." + }, + "dialogDiscardTitle": "Discard draft?", + "@dialogDiscardTitle": { + "description": "Alert dialog message to discard draft." + }, + "dialogLocationTitle": "Use Google's location service?", + "@dialogLocationTitle": { + "description": "Alert dialog title to use location services." + }, + "dialogLocationDescription": "Let Google help apps determine location. This means sending anonymous location data to Google, even when no apps are running.", + "@dialogLocationDescription": { + "description": "Alert dialog description to use location services." + }, + "dialogCancel": "CANCEL", + "@dialogCancel": { + "description": "Alert dialog cancel option." + }, + "dialogDiscard": "DISCARD", + "@dialogDiscard": { + "description": "Alert dialog discard option." + }, + "dialogDisagree": "DISAGREE", + "@dialogDisagree": { + "description": "Alert dialog disagree option." + }, + "dialogAgree": "AGREE", + "@dialogAgree": { + "description": "Alert dialog agree option." + }, + "dialogSetBackup": "Set backup account", + "@dialogSetBackup": { + "description": "Alert dialog title for setting a backup account." + }, + "dialogAddAccount": "Add account", + "@dialogAddAccount": { + "description": "Alert dialog option for adding an account." + }, + "dialogShow": "SHOW DIALOG", + "@dialogShow": { + "description": "Button text to display a dialog." + }, + "dialogFullscreenTitle": "Full Screen Dialog", + "@dialogFullscreenTitle": { + "description": "Title for full screen dialog demo." + }, + "dialogFullscreenSave": "SAVE", + "@dialogFullscreenSave": { + "description": "Save button for full screen dialog demo." + }, + "dialogFullscreenDescription": "A full screen dialog demo", + "@dialogFullscreenDescription": { + "description": "Description for full screen dialog demo." + }, + "cupertinoButton": "Button", + "@cupertinoButton": { + "description": "Button text for a generic iOS-style button." + }, + "cupertinoButtonWithBackground": "With Background", + "@cupertinoButtonWithBackground": { + "description": "Button text for a iOS-style button with a filled background." + }, + "cupertinoAlertCancel": "Cancel", + "@cupertinoAlertCancel": { + "description": "iOS-style alert cancel option." + }, + "cupertinoAlertDiscard": "Discard", + "@cupertinoAlertDiscard": { + "description": "iOS-style alert discard option." + }, + "cupertinoAlertLocationTitle": "Allow \"Maps\" to access your location while you are using the app?", + "@cupertinoAlertLocationTitle": { + "description": "iOS-style alert title for location permission." + }, + "cupertinoAlertLocationDescription": "Your current location will be displayed on the map and used for directions, nearby search results, and estimated travel times.", + "@cupertinoAlertLocationDescription": { + "description": "iOS-style alert description for location permission." + }, + "cupertinoAlertAllow": "Allow", + "@cupertinoAlertAllow": { + "description": "iOS-style alert allow option." + }, + "cupertinoAlertDontAllow": "Don't Allow", + "@cupertinoAlertDontAllow": { + "description": "iOS-style alert don't allow option." + }, + "cupertinoAlertFavoriteDessert": "Select Favorite Dessert", + "@cupertinoAlertFavoriteDessert": { + "description": "iOS-style alert title for selecting favorite dessert." + }, + "cupertinoAlertDessertDescription": "Please select your favorite type of dessert from the list below. Your selection will be used to customize the suggested list of eateries in your area.", + "@cupertinoAlertDessertDescription": { + "description": "iOS-style alert description for selecting favorite dessert." + }, + "cupertinoAlertCheesecake": "Cheesecake", + "@cupertinoAlertCheesecake": { + "description": "iOS-style alert cheesecake option." + }, + "cupertinoAlertTiramisu": "Tiramisu", + "@cupertinoAlertTiramisu": { + "description": "iOS-style alert tiramisu option." + }, + "cupertinoAlertApplePie": "Apple Pie", + "@cupertinoAlertApplePie": { + "description": "iOS-style alert apple pie option." + }, + "cupertinoAlertChocolateBrownie": "Chocolate Brownie", + "@cupertinoAlertChocolateBrownie": { + "description": "iOS-style alert chocolate brownie option." + }, + "cupertinoShowAlert": "Show Alert", + "@cupertinoShowAlert": { + "description": "Button text to show iOS-style alert." + }, + "colorsRed": "RED", + "@colorsRed": { + "description": "Tab title for the color red." + }, + "colorsPink": "PINK", + "@colorsPink": { + "description": "Tab title for the color pink." + }, + "colorsPurple": "PURPLE", + "@colorsPurple": { + "description": "Tab title for the color purple." + }, + "colorsDeepPurple": "DEEP PURPLE", + "@colorsDeepPurple": { + "description": "Tab title for the color deep purple." + }, + "colorsIndigo": "INDIGO", + "@colorsIndigo": { + "description": "Tab title for the color indigo." + }, + "colorsBlue": "BLUE", + "@colorsBlue": { + "description": "Tab title for the color blue." + }, + "colorsLightBlue": "LIGHT BLUE", + "@colorsLightBlue": { + "description": "Tab title for the color light blue." + }, + "colorsCyan": "CYAN", + "@colorsCyan": { + "description": "Tab title for the color cyan." + }, + "colorsTeal": "TEAL", + "@colorsTeal": { + "description": "Tab title for the color teal." + }, + "colorsGreen": "GREEN", + "@colorsGreen": { + "description": "Tab title for the color green." + }, + "colorsLightGreen": "LIGHT GREEN", + "@colorsLightGreen": { + "description": "Tab title for the color light green." + }, + "colorsLime": "LIME", + "@colorsLime": { + "description": "Tab title for the color lime." + }, + "colorsYellow": "YELLOW", + "@colorsYellow": { + "description": "Tab title for the color yellow." + }, + "colorsAmber": "AMBER", + "@colorsAmber": { + "description": "Tab title for the color amber." + }, + "colorsOrange": "ORANGE", + "@colorsOrange": { + "description": "Tab title for the color orange." + }, + "colorsDeepOrange": "DEEP ORANGE", + "@colorsDeepOrange": { + "description": "Tab title for the color deep orange." + }, + "colorsBrown": "BROWN", + "@colorsBrown": { + "description": "Tab title for the color brown." + }, + "colorsGrey": "GREY", + "@colorsGrey": { + "description": "Tab title for the color grey." + }, + "colorsBlueGrey": "BLUE GREY", + "@colorsBlueGrey": { + "description": "Tab title for the color blue grey." + }, + "placeChennai": "Chennai", + "@placeChennai": { + "description": "Title for Chennai location." + }, + "placeTanjore": "Tanjore", + "@placeTanjore": { + "description": "Title for Tanjore location." + }, + "placeChettinad": "Chettinad", + "@placeChettinad": { + "description": "Title for Chettinad location." + }, + "placePondicherry": "Pondicherry", + "@placePondicherry": { + "description": "Title for Pondicherry location." + }, + "placeFlowerMarket": "Flower Market", + "@placeFlowerMarket": { + "description": "Title for Flower Market location." + }, + "placeBronzeWorks": "Bronze Works", + "@placeBronzeWorks": { + "description": "Title for Bronze Works location." + }, + "placeMarket": "Market", + "@placeMarket": { + "description": "Title for Market location." + }, + "placeThanjavurTemple": "Thanjavur Temple", + "@placeThanjavurTemple": { + "description": "Title for Thanjavur Temple location." + }, + "placeSaltFarm": "Salt Farm", + "@placeSaltFarm": { + "description": "Title for Salt Farm location." + }, + "placeScooters": "Scooters", + "@placeScooters": { + "description": "Title for image of people riding on scooters." + }, + "placeSilkMaker": "Silk Maker", + "@placeSilkMaker": { + "description": "Title for an image of a silk maker." + }, + "placeLunchPrep": "Lunch Prep", + "@placeLunchPrep": { + "description": "Title for an image of preparing lunch." + }, + "placeBeach": "Beach", + "@placeBeach": { + "description": "Title for Beach location." + }, + "placeFisherman": "Fisherman", + "@placeFisherman": { + "description": "Title for an image of a fisherman." + }, + "starterAppTitle": "Starter app", + "@starterAppTitle": { + "description": "The title and name for the starter app." + }, + "starterAppDescription": "A responsive starter layout", + "@starterAppDescription": { + "description": "The description for the starter app." + }, + "starterAppGenericButton": "BUTTON", + "@starterAppGenericButton": { + "description": "Generic placeholder for button." + }, + "starterAppTooltipAdd": "Add", + "@starterAppTooltipAdd": { + "description": "Tooltip on add icon." + }, + "starterAppTooltipFavorite": "Favorite", + "@starterAppTooltipFavorite": { + "description": "Tooltip on favorite icon." + }, + "starterAppTooltipShare": "Share", + "@starterAppTooltipShare": { + "description": "Tooltip on share icon." + }, + "starterAppTooltipSearch": "Search", + "@starterAppTooltipSearch": { + "description": "Tooltip on search icon." + }, + "starterAppGenericTitle": "Title", + "@starterAppGenericTitle": { + "description": "Generic placeholder for title in app bar." + }, + "starterAppGenericSubtitle": "Subtitle", + "@starterAppGenericSubtitle": { + "description": "Generic placeholder for subtitle in drawer." + }, + "starterAppGenericHeadline": "Headline", + "@starterAppGenericHeadline": { + "description": "Generic placeholder for headline in drawer." + }, + "starterAppGenericBody": "Body", + "@starterAppGenericBody": { + "description": "Generic placeholder for body text in drawer." + }, + "starterAppDrawerItem": "Item {value}", + "@starterAppDrawerItem": { + "description": "Generic placeholder drawer item.", + "placeholders": { + "value": { + "example": "1" + } + } + }, + "shrineMenuCaption": "MENU", + "@shrineMenuCaption": { + "description": "Caption for a menu page." + }, + "shrineCategoryNameAll": "ALL", + "@shrineCategoryNameAll": { + "description": "A tab showing products from all categories." + }, + "shrineCategoryNameAccessories": "ACCESSORIES", + "@shrineCategoryNameAccessories": { + "description": "A category of products consisting of accessories (clothing items)." + }, + "shrineCategoryNameClothing": "CLOTHING", + "@shrineCategoryNameClothing": { + "description": "A category of products consisting of clothing." + }, + "shrineCategoryNameHome": "HOME", + "@shrineCategoryNameHome": { + "description": "A category of products consisting of items used at home." + }, + "shrineLogoutButtonCaption": "LOGOUT", + "@shrineLogoutButtonCaption": { + "description": "Label for a logout button." + }, + "shrineLoginUsernameLabel": "Username", + "@shrineLoginUsernameLabel": { + "description": "On the login screen, a label for a textfield for the user to input their username." + }, + "shrineLoginPasswordLabel": "Password", + "@shrineLoginPasswordLabel": { + "description": "On the login screen, a label for a textfield for the user to input their password." + }, + "shrineCancelButtonCaption": "CANCEL", + "@shrineCancelButtonCaption": { + "description": "On the login screen, the caption for a button to cancel login." + }, + "shrineNextButtonCaption": "NEXT", + "@shrineNextButtonCaption": { + "description": "On the login screen, the caption for a button to proceed login." + }, + "shrineCartPageCaption": "CART", + "@shrineCartPageCaption": { + "description": "Caption for a shopping cart page." + }, + "shrineProductQuantity": "Quantity: {quantity}", + "@shrineProductQuantity": { + "description": "A text showing the number of items for a specific product.", + "placeholders": { + "quantity": { + "example": "3" + } + } + }, + "shrineProductPrice": "x {price}", + "@shrineProductPrice": { + "description": "A text showing the unit price of each product. Used as: 'Quantity: 3 x $129'. The currency will be handled by the formatter.", + "placeholders": { + "price": { + "example": "$129" + } + } + }, + "shrineCartItemCount": "{quantity, plural, =0{NO ITEMS} =1{1 ITEM} other{{quantity} ITEMS}}", + "@shrineCartItemCount": { + "description": "A text showing the total number of items in the cart.", + "placeholders": { + "quantity": { + "example": "3" + } + } + }, + "shrineCartClearButtonCaption": "CLEAR CART", + "@shrineCartClearButtonCaption": { + "description": "Caption for a button used to clear the cart." + }, + "shrineCartTotalCaption": "TOTAL", + "@shrineCartTotalCaption": { + "description": "Label for a text showing total price of the items in the cart." + }, + "shrineCartSubtotalCaption": "Subtotal:", + "@shrineCartSubtotalCaption": { + "description": "Label for a text showing the subtotal price of the items in the cart (excluding shipping and tax)." + }, + "shrineCartShippingCaption": "Shipping:", + "@shrineCartShippingCaption": { + "description": "Label for a text showing the shipping cost for the items in the cart." + }, + "shrineCartTaxCaption": "Tax:", + "@shrineCartTaxCaption": { + "description": "Label for a text showing the tax for the items in the cart." + }, + "shrineProductVagabondSack": "Vagabond sack", + "@shrineProductVagabondSack": { + "description": "Name of the product 'Vagabond sack'." + }, + "shrineProductStellaSunglasses": "Stella sunglasses", + "@shrineProductStellaSunglasses": { + "description": "Name of the product 'Stella sunglasses'." + }, + "shrineProductWhitneyBelt": "Whitney belt", + "@shrineProductWhitneyBelt": { + "description": "Name of the product 'Whitney belt'." + }, + "shrineProductGardenStrand": "Garden strand", + "@shrineProductGardenStrand": { + "description": "Name of the product 'Garden strand'." + }, + "shrineProductStrutEarrings": "Strut earrings", + "@shrineProductStrutEarrings": { + "description": "Name of the product 'Strut earrings'." + }, + "shrineProductVarsitySocks": "Varsity socks", + "@shrineProductVarsitySocks": { + "description": "Name of the product 'Varsity socks'." + }, + "shrineProductWeaveKeyring": "Weave keyring", + "@shrineProductWeaveKeyring": { + "description": "Name of the product 'Weave keyring'." + }, + "shrineProductGatsbyHat": "Gatsby hat", + "@shrineProductGatsbyHat": { + "description": "Name of the product 'Gatsby hat'." + }, + "shrineProductShrugBag": "Shrug bag", + "@shrineProductShrugBag": { + "description": "Name of the product 'Shrug bag'." + }, + "shrineProductGiltDeskTrio": "Gilt desk trio", + "@shrineProductGiltDeskTrio": { + "description": "Name of the product 'Gilt desk trio'." + }, + "shrineProductCopperWireRack": "Copper wire rack", + "@shrineProductCopperWireRack": { + "description": "Name of the product 'Copper wire rack'." + }, + "shrineProductSootheCeramicSet": "Soothe ceramic set", + "@shrineProductSootheCeramicSet": { + "description": "Name of the product 'Soothe ceramic set'." + }, + "shrineProductHurrahsTeaSet": "Hurrahs tea set", + "@shrineProductHurrahsTeaSet": { + "description": "Name of the product 'Hurrahs tea set'." + }, + "shrineProductBlueStoneMug": "Blue stone mug", + "@shrineProductBlueStoneMug": { + "description": "Name of the product 'Blue stone mug'." + }, + "shrineProductRainwaterTray": "Rainwater tray", + "@shrineProductRainwaterTray": { + "description": "Name of the product 'Rainwater tray'." + }, + "shrineProductChambrayNapkins": "Chambray napkins", + "@shrineProductChambrayNapkins": { + "description": "Name of the product 'Chambray napkins'." + }, + "shrineProductSucculentPlanters": "Succulent planters", + "@shrineProductSucculentPlanters": { + "description": "Name of the product 'Succulent planters'." + }, + "shrineProductQuartetTable": "Quartet table", + "@shrineProductQuartetTable": { + "description": "Name of the product 'Quartet table'." + }, + "shrineProductKitchenQuattro": "Kitchen quattro", + "@shrineProductKitchenQuattro": { + "description": "Name of the product 'Kitchen quattro'." + }, + "shrineProductClaySweater": "Clay sweater", + "@shrineProductClaySweater": { + "description": "Name of the product 'Clay sweater'." + }, + "shrineProductSeaTunic": "Sea tunic", + "@shrineProductSeaTunic": { + "description": "Name of the product 'Sea tunic'." + }, + "shrineProductPlasterTunic": "Plaster tunic", + "@shrineProductPlasterTunic": { + "description": "Name of the product 'Plaster tunic'." + }, + "shrineProductWhitePinstripeShirt": "White pinstripe shirt", + "@shrineProductWhitePinstripeShirt": { + "description": "Name of the product 'White pinstripe shirt'." + }, + "shrineProductChambrayShirt": "Chambray shirt", + "@shrineProductChambrayShirt": { + "description": "Name of the product 'Chambray shirt'." + }, + "shrineProductSeabreezeSweater": "Seabreeze sweater", + "@shrineProductSeabreezeSweater": { + "description": "Name of the product 'Seabreeze sweater'." + }, + "shrineProductGentryJacket": "Gentry jacket", + "@shrineProductGentryJacket": { + "description": "Name of the product 'Gentry jacket'." + }, + "shrineProductNavyTrousers": "Navy trousers", + "@shrineProductNavyTrousers": { + "description": "Name of the product 'Navy trousers'." + }, + "shrineProductWalterHenleyWhite": "Walter henley (white)", + "@shrineProductWalterHenleyWhite": { + "description": "Name of the product 'Walter henley (white)'." + }, + "shrineProductSurfAndPerfShirt": "Surf and perf shirt", + "@shrineProductSurfAndPerfShirt": { + "description": "Name of the product 'Surf and perf shirt'." + }, + "shrineProductGingerScarf": "Ginger scarf", + "@shrineProductGingerScarf": { + "description": "Name of the product 'Ginger scarf'." + }, + "shrineProductRamonaCrossover": "Ramona crossover", + "@shrineProductRamonaCrossover": { + "description": "Name of the product 'Ramona crossover'." + }, + "shrineProductChambrayShirt": "Chambray shirt", + "@shrineProductChambrayShirt": { + "description": "Name of the product 'Chambray shirt'." + }, + "shrineProductClassicWhiteCollar": "Classic white collar", + "@shrineProductClassicWhiteCollar": { + "description": "Name of the product 'Classic white collar'." + }, + "shrineProductCeriseScallopTee": "Cerise scallop tee", + "@shrineProductCeriseScallopTee": { + "description": "Name of the product 'Cerise scallop tee'." + }, + "shrineProductShoulderRollsTee": "Shoulder rolls tee", + "@shrineProductShoulderRollsTee": { + "description": "Name of the product 'Shoulder rolls tee'." + }, + "shrineProductGreySlouchTank": "Grey slouch tank", + "@shrineProductGreySlouchTank": { + "description": "Name of the product 'Grey slouch tank'." + }, + "shrineProductSunshirtDress": "Sunshirt dress", + "@shrineProductSunshirtDress": { + "description": "Name of the product 'Sunshirt dress'." + }, + "shrineProductFineLinesTee": "Fine lines tee", + "@shrineProductFineLinesTee": { + "description": "Name of the product 'Fine lines tee'." + }, + "shrineTooltipSearch": "Search", + "@shrineTooltipSearch": { + "description": "The tooltip text for a search button. Also used as a semantic label, used by screen readers, such as TalkBack and VoiceOver." + }, + "shrineTooltipSettings": "Settings", + "@shrineTooltipSettings": { + "description": "The tooltip text for a settings button. Also used as a semantic label, used by screen readers, such as TalkBack and VoiceOver." + }, + "shrineTooltipOpenMenu": "Open menu", + "@shrineTooltipOpenMenu": { + "description": "The tooltip text for a menu button. Also used as a semantic label, used by screen readers, such as TalkBack and VoiceOver." + }, + "shrineTooltipCloseMenu": "Close menu", + "@shrineTooltipCloseMenu": { + "description": "The tooltip text for a button to close a menu. Also used as a semantic label, used by screen readers, such as TalkBack and VoiceOver." + }, + "shrineTooltipCloseCart": "Close cart", + "@shrineTooltipCloseCart": { + "description": "The tooltip text for a button to close the shopping cart page. Also used as a semantic label, used by screen readers, such as TalkBack and VoiceOver." + }, + "shrineScreenReaderCart": "{quantity, plural, =0{Shopping cart, no items} =1{Shopping cart, 1 item} other{Shopping cart, {quantity} items}}", + "@shrineScreenReaderCart": { + "description": "The description of a shopping cart button containing some products. Used by screen readers, such as TalkBack and VoiceOver.", + "placeholders": { + "quantity": { + "example": "3" + } + } + }, + "shrineScreenReaderProductAddToCart": "Add to cart", + "@shrineScreenReaderProductAddToCart": { + "description": "An announcement made by screen readers, such as TalkBack and VoiceOver to indicate the action of a button for adding a product to the cart." + }, + "shrineScreenReaderRemoveProductButton": "Remove {product}", + "@shrineScreenReaderRemoveProductButton": { + "description": "A tooltip for a button to remove a product. This will be read by screen readers, such as TalkBack and VoiceOver when a product is added to the shopping cart.", + "placeholders": { + "product": { + "example": "Ginger scarf" + } + } + }, + "shrineTooltipRemoveItem": "Remove item", + "@shrineTooltipRemoveItem": { + "description": "The tooltip text for a button to remove an item (a product) in a shopping cart. Also used as a semantic label, used by screen readers, such as TalkBack and VoiceOver." + }, + "craneFormDiners": "Diners", + "@craneFormDiners": { + "description": "Form field label to enter the number of diners." + }, + "craneFormDate": "Select Date", + "@craneFormDate": { + "description": "Form field label to select a date." + }, + "craneFormTime": "Select Time", + "@craneFormTime": { + "description": "Form field label to select a time." + }, + "craneFormLocation": "Select Location", + "@craneFormLocation": { + "description": "Form field label to select a location." + }, + "craneFormTravelers": "Travelers", + "@craneFormTravelers": { + "description": "Form field label to select the number of travellers." + }, + "craneFormOrigin": "Choose Origin", + "@craneFormOrigin": { + "description": "Form field label to choose a travel origin." + }, + "craneFormDestination": "Choose Destination", + "@craneFormDestination": { + "description": "Form field label to choose a travel destination." + }, + "craneFormDates": "Select Dates", + "@craneFormDates": { + "description": "Form field label to select multiple dates." + }, + "craneHours": "{hours, plural, =1{1h} other{{hours}h}}", + "@craneHours": { + "description": "Generic text for an amount of hours, abbreviated to the shortest form. For example 1h. {hours} should remain untranslated.", + "placeholders": { + "hours": { + "example": "1" + } + } + }, + "craneMinutes": "{minutes, plural, =1{1m} other{{minutes}m}}", + "@craneMinutes": { + "description": "Generic text for an amount of minutes, abbreviated to the shortest form. For example 15m. {minutes} should remain untranslated.", + "placeholders": { + "minutes": { + "example": "15" + } + } + }, + "craneFlightDuration": "{hoursShortForm} {minutesShortForm}", + "@craneFlightDuration": { + "description": "A pattern to define the layout of a flight duration string. For example in English one might say 1h 15m. Translation should only rearrange the inputs. {hoursShortForm} would for example be replaced by 1h, already translated to the given locale. {minutesShortForm} would for example be replaced by 15m, already translated to the given locale.", + "placeholders": { + "hoursShortForm": { + "example": "1h" + }, + "minutesShortForm": { + "example": "15m" + } + } + }, + "craneFly": "FLY", + "@craneFly": { + "description": "Title for FLY tab." + }, + "craneSleep": "SLEEP", + "@craneSleep": { + "description": "Title for SLEEP tab." + }, + "craneEat": "EAT", + "@craneEat": { + "description": "Title for EAT tab." + }, + "craneFlySubhead": "Explore Flights by Destination", + "@craneFlySubhead": { + "description": "Subhead for FLY tab." + }, + "craneSleepSubhead": "Explore Properties by Destination", + "@craneSleepSubhead": { + "description": "Subhead for SLEEP tab." + }, + "craneEatSubhead": "Explore Restaurants by Destination", + "@craneEatSubhead": { + "description": "Subhead for EAT tab." + }, + "craneFlyStops": "{numberOfStops, plural, =0{Nonstop} =1{1 stop} other{{numberOfStops} stops}}", + "@craneFlyStops": { + "description": "Label indicating if a flight is nonstop or how many layovers it includes.", + "placeholders": { + "numberOfStops": { + "example": "2" + } + } + }, + "craneSleepProperties": "{totalProperties, plural, =0{No Available Properties} =1{1 Available Properties} other{{totalProperties} Available Properties}}", + "@craneSleepProperties": { + "description": "Text indicating the number of available properties (temporary rentals). Always plural.", + "placeholders": { + "totalProperties": { + "example": "100" + } + } + }, + "craneEatRestaurants": "{totalRestaurants, plural, =0{No Restaurants} =1{1 Restaurant} other{{totalRestaurants} Restaurants}}", + "@craneEatRestaurants": { + "description": "Text indicating the number of restaurants. Always plural.", + "placeholders": { + "totalRestaurants": { + "example": "100" + } + } + }, + "craneFly0": "Aspen, United States", + "@craneFly0": { + "description": "Label for city." + }, + "craneFly1": "Big Sur, United States", + "@craneFly1": { + "description": "Label for city." + }, + "craneFly2": "Khumbu Valley, Nepal", + "@craneFly2": { + "description": "Label for city." + }, + "craneFly3": "Machu Picchu, Peru", + "@craneFly3": { + "description": "Label for city." + }, + "craneFly4": "Malé, Maldives", + "@craneFly4": { + "description": "Label for city." + }, + "craneFly5": "Vitznau, Switzerland", + "@craneFly5": { + "description": "Label for city." + }, + "craneFly6": "Mexico City, Mexico", + "@craneFly6": { + "description": "Label for city." + }, + "craneFly7": "Mount Rushmore, United States", + "@craneFly7": { + "description": "Label for city." + }, + "craneFly8": "Singapore", + "@craneFly8": { + "description": "Label for city." + }, + "craneFly9": "Havana, Cuba", + "@craneFly9": { + "description": "Label for city." + }, + "craneFly10": "Cairo, Egypt", + "@craneFly10": { + "description": "Label for city." + }, + "craneFly11": "Lisbon, Portugal", + "@craneFly11": { + "description": "Label for city." + }, + "craneFly12": "Napa, United States", + "@craneFly12": { + "description": "Label for city." + }, + "craneFly13": "Bali, Indonesia", + "@craneFly13": { + "description": "Label for city." + }, + "craneSleep0": "Malé, Maldives", + "@craneSleep0": { + "description": "Label for city." + }, + "craneSleep1": "Aspen, United States", + "@craneSleep1": { + "description": "Label for city." + }, + "craneSleep2": "Machu Picchu, Peru", + "@craneSleep2": { + "description": "Label for city." + }, + "craneSleep3": "Havana, Cuba", + "@craneSleep3": { + "description": "Label for city." + }, + "craneSleep4": "Vitznau, Switzerland", + "@craneSleep4": { + "description": "Label for city." + }, + "craneSleep5": "Big Sur, United States", + "@craneSleep5": { + "description": "Label for city." + }, + "craneSleep6": "Napa, United States", + "@craneSleep6": { + "description": "Label for city." + }, + "craneSleep7": "Porto, Portugal", + "@craneSleep7": { + "description": "Label for city." + }, + "craneSleep8": "Tulum, Mexico", + "@craneSleep8": { + "description": "Label for city." + }, + "craneSleep9": "Lisbon, Portugal", + "@craneSleep9": { + "description": "Label for city." + }, + "craneSleep10": "Cairo, Egypt", + "@craneSleep10": { + "description": "Label for city." + }, + "craneSleep11": "Taipei, Taiwan", + "@craneSleep11": { + "description": "Label for city." + }, + "craneEat0": "Naples, Italy", + "@craneEat0": { + "description": "Label for city." + }, + "craneEat1": "Dallas, United States", + "@craneEat1": { + "description": "Label for city." + }, + "craneEat2": "Córdoba, Argentina", + "@craneEat2": { + "description": "Label for city." + }, + "craneEat3": "Portland, United States", + "@craneEat3": { + "description": "Label for city." + }, + "craneEat4": "Paris, France", + "@craneEat4": { + "description": "Label for city." + }, + "craneEat5": "Seoul, South Korea", + "@craneEat5": { + "description": "Label for city." + }, + "craneEat6": "Seattle, United States", + "@craneEat6": { + "description": "Label for city." + }, + "craneEat7": "Nashville, United States", + "@craneEat7": { + "description": "Label for city." + }, + "craneEat8": "Atlanta, United States", + "@craneEat8": { + "description": "Label for city." + }, + "craneEat9": "Madrid, Spain", + "@craneEat9": { + "description": "Label for city." + }, + "craneEat10": "Lisbon, Portugal", + "@craneEat10": { + "description": "Label for city." + }, + "craneFly0SemanticLabel": "Chalet in a snowy landscape with evergreen trees", + "@craneFly0SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneFly1SemanticLabel": "Tent in a field", + "@craneFly1SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneFly2SemanticLabel": "Prayer flags in front of snowy mountain", + "@craneFly2SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneFly3SemanticLabel": "Machu Picchu citadel", + "@craneFly3SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneFly4SemanticLabel": "Overwater bungalows", + "@craneFly4SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneFly5SemanticLabel": "Lake-side hotel in front of mountains", + "@craneFly5SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneFly6SemanticLabel": "Aerial view of Palacio de Bellas Artes", + "@craneFly6SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneFly7SemanticLabel": "Mount Rushmore", + "@craneFly7SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneFly8SemanticLabel": "Supertree Grove", + "@craneFly8SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneFly9SemanticLabel": "Man leaning on an antique blue car", + "@craneFly9SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneFly10SemanticLabel": "Al-Azhar Mosque towers during sunset", + "@craneFly10SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneFly11SemanticLabel": "Brick lighthouse at sea", + "@craneFly11SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneFly12SemanticLabel": "Pool with palm trees", + "@craneFly12SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneFly13SemanticLabel": "Sea-side pool with palm trees", + "@craneFly13SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneSleep0SemanticLabel": "Overwater bungalows", + "@craneSleep0SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneSleep1SemanticLabel": "Chalet in a snowy landscape with evergreen trees", + "@craneSleep1SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneSleep2SemanticLabel": "Machu Picchu citadel", + "@craneSleep2SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneSleep3SemanticLabel": "Man leaning on an antique blue car", + "@craneSleep3SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneSleep4SemanticLabel": "Lake-side hotel in front of mountains", + "@craneSleep4SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneSleep5SemanticLabel": "Tent in a field", + "@craneSleep5SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneSleep6SemanticLabel": "Pool with palm trees", + "@craneSleep6SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneSleep7SemanticLabel": "Colorful apartments at Riberia Square", + "@craneSleep7SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneSleep8SemanticLabel": "Mayan ruins on a cliff above a beach", + "@craneSleep8SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneSleep9SemanticLabel": "Brick lighthouse at sea", + "@craneSleep9SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneSleep10SemanticLabel": "Al-Azhar Mosque towers during sunset", + "@craneSleep10SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneSleep11SemanticLabel": "Taipei 101 skyscraper", + "@craneSleep11SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneEat0SemanticLabel": "Pizza in a wood-fired oven", + "@craneEat0SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneEat1SemanticLabel": "Empty bar with diner-style stools", + "@craneEat1SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneEat2SemanticLabel": "Burger", + "@craneEat2SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneEat3SemanticLabel": "Korean taco", + "@craneEat3SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneEat4SemanticLabel": "Chocolate dessert", + "@craneEat4SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneEat5SemanticLabel": "Artsy restaurant seating area", + "@craneEat5SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneEat6SemanticLabel": "Shrimp dish", + "@craneEat6SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneEat7SemanticLabel": "Bakery entrance", + "@craneEat7SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneEat8SemanticLabel": "Plate of crawfish", + "@craneEat8SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneEat9SemanticLabel": "Cafe counter with pastries", + "@craneEat9SemanticLabel": { + "description": "Semantic label for an image." + }, + "craneEat10SemanticLabel": "Woman holding huge pastrami sandwich", + "@craneEat10SemanticLabel": { + "description": "Semantic label for an image." + }, + "fortnightlyMenuFrontPage": "Front Page", + "@fortnightlyMenuFrontPage": { + "description": "Menu item for the front page of the news app." + }, + "fortnightlyMenuWorld": "World", + "@fortnightlyMenuWorld": { + "description": "Menu item for the world news section of the news app." + }, + "fortnightlyMenuUS": "US", + "@fortnightlyMenuUS": { + "description": "Menu item for the United States news section of the news app." + }, + "fortnightlyMenuPolitics": "Politics", + "@fortnightlyMenuPolitics": { + "description": "Menu item for the political news section of the news app." + }, + "fortnightlyMenuBusiness": "Business", + "@fortnightlyMenuBusiness": { + "description": "Menu item for the business news section of the news app." + }, + "fortnightlyMenuTech": "Tech", + "@fortnightlyMenuTech": { + "description": "Menu item for the tech news section of the news app." + }, + "fortnightlyMenuScience": "Science", + "@fortnightlyMenuScience": { + "description": "Menu item for the science news section of the news app." + }, + "fortnightlyMenuSports": "Sports", + "@fortnightlyMenuSports": { + "description": "Menu item for the sports news section of the news app." + }, + "fortnightlyMenuTravel": "Travel", + "@fortnightlyMenuTravel": { + "description": "Menu item for the travel news section of the news app." + }, + "fortnightlyMenuCulture": "Culture", + "@fortnightlyMenuCulture": { + "description": "Menu item for the culture news section of the news app." + }, + "fortnightlyTrendingTechDesign": "TechDesign", + "@fortnightlyTrendingTechDesign": { + "description": "Hashtag for the tech design trending topic of the news app." + }, + "fortnightlyTrendingReform": "Reform", + "@fortnightlyTrendingReform": { + "description": "Hashtag for the reform trending topic of the news app." + }, + "fortnightlyTrendingHealthcareRevolution": "HealthcareRevolution", + "@fortnightlyTrendingHealthcareRevolution": { + "description": "Hashtag for the healthcare revolution trending topic of the news app." + }, + "fortnightlyTrendingGreenArmy": "GreenArmy", + "@fortnightlyTrendingGreenArmy": { + "description": "Hashtag for the green army trending topic of the news app." + }, + "fortnightlyTrendingStocks": "Stocks", + "@fortnightlyTrendingStocks": { + "description": "Hashtag for the stocks trending topic of the news app." + }, + "fortnightlyLatestUpdates": "Latest Updates", + "@fortnightlyLatestUpdates": { + "description": "Title for news section regarding the latest updates." + }, + "fortnightlyHeadlineHealthcare": "The Quiet, Yet Powerful Healthcare Revolution", + "@fortnightlyHeadlineHealthcare": { + "description": "Headline for a news article about healthcare." + }, + "fortnightlyHeadlineWar": "Divided American Lives During War", + "@fortnightlyHeadlineWar": { + "description": "Headline for a news article about war." + }, + "fortnightlyHeadlineGasoline": "The Future of Gasoline", + "@fortnightlyHeadlineGasoline": { + "description": "Headline for a news article about gasoline." + }, + "fortnightlyHeadlineArmy": "Reforming The Green Army From Within", + "@fortnightlyHeadlineArmy": { + "description": "Headline for a news article about the green army." + }, + "fortnightlyHeadlineStocks": "As Stocks Stagnate, Many Look To Currency", + "@fortnightlyHeadlineStocks": { + "description": "Headline for a news article about stocks." + }, + "fortnightlyHeadlineFabrics": "Designers Use Tech To Make Futuristic Fabrics", + "@fortnightlyHeadlineFabrics": { + "description": "Headline for a news article about fabric." + }, + "fortnightlyHeadlineFeminists": "Feminists Take On Partisanship", + "@fortnightlyHeadlineFeminists": { + "description": "Headline for a news article about feminists and partisanship." + }, + "fortnightlyHeadlineBees": "Farmland Bees In Short Supply", + "@fortnightlyHeadlineBees": { + "description": "Headline for a news article about bees." + }, + "replyInboxLabel": "Inbox", + "@replyInboxLabel": { + "description": "Text label for Inbox destination." + }, + "replyStarredLabel": "Starred", + "@replyStarredLabel": { + "description": "Text label for Starred destination." + }, + "replySentLabel": "Sent", + "@replySentLabel": { + "description": "Text label for Sent destination." + }, + "replyTrashLabel": "Trash", + "@replyTrashLabel": { + "description": "Text label for Trash destination." + }, + "replySpamLabel": "Spam", + "@replySpamLabel": { + "description": "Text label for Spam destination." + }, + "replyDraftsLabel": "Drafts", + "@replyDraftsLabel": { + "description": "Text label for Drafts destination." + }, + "demoTwoPaneFoldableLabel": "Foldable", + "@demoTwoPaneFoldableLabel": { + "description": "Option title for TwoPane demo on foldable devices." + }, + "demoTwoPaneFoldableDescription": "This is how TwoPane behaves on a foldable device.", + "@demoTwoPaneFoldableDescription": { + "description": "Description for the foldable option configuration on the TwoPane demo." + }, + "demoTwoPaneSmallScreenLabel": "Small Screen", + "@demoTwoPaneSmallScreenLabel": { + "description": "Option title for TwoPane demo in small screen mode. Counterpart of the foldable option." + }, + "demoTwoPaneSmallScreenDescription": "This is how TwoPane behaves on a small screen device.", + "@demoTwoPaneSmallScreenDescription": { + "description": "Description for the small screen option configuration on the TwoPane demo." + }, + "demoTwoPaneTabletLabel": "Tablet / Desktop", + "@demoTwoPaneTabletLabel": { + "description": "Option title for TwoPane demo in tablet or desktop mode." + }, + "demoTwoPaneTabletDescription": "This is how TwoPane behaves on a larger screen like a tablet or desktop.", + "@demoTwoPaneTabletDescription": { + "description": "Description for the tablet / desktop option configuration on the TwoPane demo." + }, + "demoTwoPaneTitle": "TwoPane", + "@demoTwoPaneTitle": { + "description": "Title for the TwoPane widget demo." + }, + "demoTwoPaneSubtitle": "Responsive layouts on foldable, large, and small screens", + "@demoTwoPaneSubtitle": { + "description": "Subtitle for the TwoPane widget demo." + }, + "splashSelectDemo": "Select a demo", + "@splashSelectDemo": { + "description": "Tip for user, visible on the right side of the splash screen when Gallery runs on a foldable device." + }, + "demoTwoPaneList": "List", + "@demoTwoPaneList": { + "description": "Title of one of the panes in the TwoPane demo. It sits on top of a list of items." + }, + "demoTwoPaneDetails": "Details", + "@demoTwoPaneDetails": { + "description": "Title of one of the panes in the TwoPane demo, which shows details of the currently selected item." + }, + "demoTwoPaneSelectItem": "Select an item", + "@demoTwoPaneSelectItem": { + "description": "Tip for user, visible on the right side of the TwoPane widget demo in the foldable configuration." + }, + "demoTwoPaneItem": "Item {value}", + "@demoTwoPaneItem": { + "description": "Generic item placeholder visible in the TwoPane widget demo.", + "placeholders": { + "value": { + "example": "1" + } + } + }, + "demoTwoPaneItemDetails": "Item {value} details", + "@demoTwoPaneItemDetails": { + "description": "Generic item description or details visible in the TwoPane widget demo.", + "placeholders": { + "value": { + "example": "1" + } + } + } +} diff --git a/dev/integration_tests/new_gallery/lib/l10n/intl_en_IS.arb b/dev/integration_tests/new_gallery/lib/l10n/intl_en_IS.arb new file mode 100644 index 0000000000..49d03c35a8 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/l10n/intl_en_IS.arb @@ -0,0 +1,828 @@ +{ + "loading": "Loading", + "deselect": "Deselect", + "select": "Select", + "selectable": "Selectable (long press)", + "selected": "Selected", + "demo": "Demo", + "bottomAppBar": "Bottom app bar", + "notSelected": "Not selected", + "demoCupertinoSearchTextFieldTitle": "Search text field", + "demoCupertinoPicker": "Picker", + "demoCupertinoSearchTextFieldSubtitle": "iOS-style search text field", + "demoCupertinoSearchTextFieldDescription": "A search text field that lets the user search by entering text and that can offer and filter suggestions.", + "demoCupertinoSearchTextFieldPlaceholder": "Enter some text", + "demoCupertinoScrollbarTitle": "Scrollbar", + "demoCupertinoScrollbarSubtitle": "iOS-style scrollbar", + "demoCupertinoScrollbarDescription": "A scrollbar that wraps the given child", + "demoTwoPaneItem": "Item {value}", + "demoTwoPaneList": "List", + "demoTwoPaneFoldableLabel": "Foldable", + "demoTwoPaneSmallScreenLabel": "Small screen", + "demoTwoPaneSmallScreenDescription": "This is how TwoPane behaves on a small screen device.", + "demoTwoPaneTabletLabel": "Tablet/Desktop", + "demoTwoPaneTabletDescription": "This is how TwoPane behaves on a larger screen like a tablet or desktop.", + "demoTwoPaneTitle": "TwoPane", + "demoTwoPaneSubtitle": "Responsive layouts on foldable, large and small screens", + "splashSelectDemo": "Select a demo", + "demoTwoPaneFoldableDescription": "This is how TwoPane behaves on a foldable device.", + "demoTwoPaneDetails": "Details", + "demoTwoPaneSelectItem": "Select an item", + "demoTwoPaneItemDetails": "Item {value} details", + "demoCupertinoContextMenuActionText": "Tap and hold the Flutter logo to see the context menu.", + "demoCupertinoContextMenuDescription": "An iOS-style full screen contextual menu that appears when an element is long-pressed.", + "demoAppBarTitle": "App bar", + "demoAppBarDescription": "The app bar provides content and actions related to the current screen. It's used for branding, screen titles, navigation and actions", + "demoDividerTitle": "Divider", + "demoDividerSubtitle": "A divider is a thin line that groups content in lists and layouts.", + "demoDividerDescription": "Dividers can be used in lists, drawers and elsewhere to separate content.", + "demoVerticalDividerTitle": "Vertical divider", + "demoCupertinoContextMenuTitle": "Context menu", + "demoCupertinoContextMenuSubtitle": "iOS-style context menu", + "demoAppBarSubtitle": "Displays information and actions relating to the current screen", + "demoCupertinoContextMenuActionOne": "Action one", + "demoCupertinoContextMenuActionTwo": "Action two", + "demoDateRangePickerDescription": "Shows a dialogue containing a Material Design date range picker.", + "demoDateRangePickerTitle": "Date range picker", + "demoNavigationDrawerUserName": "User name", + "demoNavigationDrawerUserEmail": "user.name@example.com", + "demoNavigationDrawerText": "Swipe from the edge or tap the upper-left icon to see the drawer", + "demoNavigationRailTitle": "Navigation rail", + "demoNavigationRailSubtitle": "Displaying a navigation rail within an app", + "demoNavigationRailDescription": "A material widget that is meant to be displayed at the left or right of an app to navigate between a small number of views, typically between three and five.", + "demoNavigationRailFirst": "First", + "demoNavigationDrawerTitle": "Navigation drawer", + "demoNavigationRailThird": "Third", + "replyStarredLabel": "Starred", + "demoTextButtonDescription": "A text button displays an ink splash on press but does not lift. Use text buttons on toolbars, in dialogues and inline with padding", + "demoElevatedButtonTitle": "Elevated button", + "demoElevatedButtonDescription": "Elevated buttons add dimension to mostly flat layouts. They emphasise functions on busy or wide spaces.", + "demoOutlinedButtonTitle": "Outlined button", + "demoOutlinedButtonDescription": "Outlined buttons become opaque and elevate when pressed. They are often paired with raised buttons to indicate an alternative, secondary action.", + "demoContainerTransformDemoInstructions": "Cards, lists and FAB", + "demoNavigationDrawerSubtitle": "Displaying a drawer within app bar", + "replyDescription": "An efficient, focused email app", + "demoNavigationDrawerDescription": "A Material Design panel that slides in horizontally from the edge of the screen to show navigation links in an application.", + "replyDraftsLabel": "Drafts", + "demoNavigationDrawerToPageOne": "Item one", + "replyInboxLabel": "Inbox", + "demoSharedXAxisDemoInstructions": "Next and back buttons", + "replySpamLabel": "Spam", + "replyTrashLabel": "Bin", + "replySentLabel": "Sent", + "demoNavigationRailSecond": "Second", + "demoNavigationDrawerToPageTwo": "Item two", + "demoFadeScaleDemoInstructions": "Modal and FAB", + "demoFadeThroughDemoInstructions": "Bottom navigation", + "demoSharedZAxisDemoInstructions": "Settings icon button", + "demoSharedYAxisDemoInstructions": "Sort by 'Recently played'", + "demoTextButtonTitle": "Text button", + "demoSharedZAxisBeefSandwichRecipeTitle": "Beef sandwich", + "demoSharedZAxisDessertRecipeDescription": "Dessert recipe", + "demoSharedYAxisAlbumTileSubtitle": "Artist", + "demoSharedYAxisAlbumTileTitle": "Album", + "demoSharedYAxisRecentSortTitle": "Recently played", + "demoSharedYAxisAlphabeticalSortTitle": "A–Z", + "demoSharedYAxisAlbumCount": "268 albums", + "demoSharedYAxisTitle": "Shared y-axis", + "demoSharedXAxisCreateAccountButtonText": "CREATE ACCOUNT", + "demoFadeScaleAlertDialogDiscardButton": "DISCARD", + "demoSharedXAxisSignInTextFieldLabel": "Email or phone number", + "demoSharedXAxisSignInSubtitleText": "Sign in with your account", + "demoSharedXAxisSignInWelcomeText": "Hi David Park", + "demoSharedXAxisIndividualCourseSubtitle": "Shown individually", + "demoSharedXAxisBundledCourseSubtitle": "Bundled", + "demoFadeThroughAlbumsDestination": "Albums", + "demoSharedXAxisDesignCourseTitle": "Design", + "demoSharedXAxisIllustrationCourseTitle": "Illustration", + "demoSharedXAxisBusinessCourseTitle": "Business", + "demoSharedXAxisArtsAndCraftsCourseTitle": "Arts and crafts", + "demoMotionPlaceholderSubtitle": "Secondary text", + "demoFadeScaleAlertDialogCancelButton": "CANCEL", + "demoFadeScaleAlertDialogHeader": "Alert dialogue", + "demoFadeScaleHideFabButton": "HIDE FAB", + "demoFadeScaleShowFabButton": "SHOW FAB", + "demoFadeScaleShowAlertDialogButton": "SHOW MODAL", + "demoFadeScaleDescription": "The fade pattern is used for UI elements that enter or exit within the bounds of the screen, such as a dialogue that fades in the centre of the screen.", + "demoFadeScaleTitle": "Fade", + "demoFadeThroughTextPlaceholder": "123 photos", + "demoFadeThroughSearchDestination": "Search", + "demoFadeThroughPhotosDestination": "Photos", + "demoSharedXAxisCoursePageSubtitle": "Bundled categories appear as groups in your feed. You can always change this later.", + "demoFadeThroughDescription": "The fade-through pattern is used for transitions between UI elements that do not have a strong relationship to each other.", + "demoFadeThroughTitle": "Fade through", + "demoSharedZAxisHelpSettingLabel": "Help", + "demoMotionSubtitle": "All of the predefined transition patterns", + "demoSharedZAxisNotificationSettingLabel": "Notifications", + "demoSharedZAxisProfileSettingLabel": "Profile", + "demoSharedZAxisSavedRecipesListTitle": "Saved recipes", + "demoSharedZAxisBeefSandwichRecipeDescription": "Beef sandwich recipe", + "demoSharedZAxisCrabPlateRecipeDescription": "Crab plate recipe", + "demoSharedXAxisCoursePageTitle": "Streamline your courses", + "demoSharedZAxisCrabPlateRecipeTitle": "Crab", + "demoSharedZAxisShrimpPlateRecipeDescription": "Shrimp plate recipe", + "demoSharedZAxisShrimpPlateRecipeTitle": "Shrimp", + "demoContainerTransformTypeFadeThrough": "FADE THROUGH", + "demoSharedZAxisDessertRecipeTitle": "Dessert", + "demoSharedZAxisSandwichRecipeDescription": "Sandwich recipe", + "demoSharedZAxisSandwichRecipeTitle": "Sandwich", + "demoSharedZAxisBurgerRecipeDescription": "Burger recipe", + "demoSharedZAxisBurgerRecipeTitle": "Burger", + "demoSharedZAxisSettingsPageTitle": "Settings", + "demoSharedZAxisTitle": "Shared z-axis", + "demoSharedZAxisPrivacySettingLabel": "Privacy", + "demoMotionTitle": "Motion", + "demoContainerTransformTitle": "Container transform", + "demoContainerTransformDescription": "The container transform pattern is designed for transitions between UI elements that include a container. This pattern creates a visible connection between two UI elements", + "demoContainerTransformModalBottomSheetTitle": "Fade mode", + "demoContainerTransformTypeFade": "FADE", + "demoSharedYAxisAlbumTileDurationUnit": "min", + "demoMotionPlaceholderTitle": "Title", + "demoSharedXAxisForgotEmailButtonText": "FORGOT EMAIL?", + "demoMotionSmallPlaceholderSubtitle": "Secondary", + "demoMotionDetailsPageTitle": "Details page", + "demoMotionListTileTitle": "List item", + "demoSharedAxisDescription": "The shared axis pattern is used for transitions between the UI elements that have a spatial or navigational relationship. This pattern uses a shared transformation on the x, y or z axis to reinforce the relationship between elements.", + "demoSharedXAxisTitle": "Shared x-axis", + "demoSharedXAxisBackButtonText": "BACK", + "demoSharedXAxisNextButtonText": "NEXT", + "demoSharedXAxisCulinaryCourseTitle": "Culinary", + "githubRepo": "{repoName} GitHub repository", + "fortnightlyMenuUS": "US", + "fortnightlyMenuBusiness": "Business", + "fortnightlyMenuScience": "Science", + "fortnightlyMenuSports": "Sport", + "fortnightlyMenuTravel": "Travel", + "fortnightlyMenuCulture": "Culture", + "fortnightlyTrendingTechDesign": "TechDesign", + "rallyBudgetDetailAmountLeft": "Amount left", + "fortnightlyHeadlineArmy": "Reforming The Green Army from Within", + "fortnightlyDescription": "A content-focused news app", + "rallyBillDetailAmountDue": "Amount due", + "rallyBudgetDetailTotalCap": "Total cap", + "rallyBudgetDetailAmountUsed": "Amount used", + "fortnightlyTrendingHealthcareRevolution": "HealthcareRevolution", + "fortnightlyMenuFrontPage": "Front page", + "fortnightlyMenuWorld": "World", + "rallyBillDetailAmountPaid": "Amount paid", + "fortnightlyMenuPolitics": "Politics", + "fortnightlyHeadlineBees": "Farmland Bees in Short Supply", + "fortnightlyHeadlineGasoline": "The Future of Petrol", + "fortnightlyTrendingGreenArmy": "GreenArmy", + "fortnightlyHeadlineFeminists": "Feminists take on Partisanship", + "fortnightlyHeadlineFabrics": "Designers use Tech to make Futuristic Fabrics", + "fortnightlyHeadlineStocks": "As Stocks Stagnate, many Look to Currency", + "fortnightlyTrendingReform": "Reform", + "fortnightlyMenuTech": "Tech", + "fortnightlyHeadlineWar": "Divided American Lives During War", + "fortnightlyHeadlineHealthcare": "The Quiet, yet Powerful Healthcare Revolution", + "fortnightlyLatestUpdates": "Latest updates", + "fortnightlyTrendingStocks": "Stocks", + "rallyBillDetailTotalAmount": "Total amount", + "demoCupertinoPickerDateTime": "Date and time", + "signIn": "SIGN IN", + "dataTableRowWithSugar": "{value} with sugar", + "dataTableRowApplePie": "Apple pie", + "dataTableRowDonut": "Doughnut", + "dataTableRowHoneycomb": "Honeycomb", + "dataTableRowLollipop": "Lollipop", + "dataTableRowJellyBean": "Jelly bean", + "dataTableRowGingerbread": "Gingerbread", + "dataTableRowCupcake": "Cupcake", + "dataTableRowEclair": "Eclair", + "dataTableRowIceCreamSandwich": "Ice cream sandwich", + "dataTableRowFrozenYogurt": "Frozen yogurt", + "dataTableColumnIron": "Iron (%)", + "dataTableColumnCalcium": "Calcium (%)", + "dataTableColumnSodium": "Sodium (mg)", + "demoTimePickerTitle": "Time picker", + "demo2dTransformationsResetTooltip": "Reset transformations", + "dataTableColumnFat": "Fat (gm)", + "dataTableColumnCalories": "Calories", + "dataTableColumnDessert": "Dessert (1 serving)", + "cardsDemoTravelDestinationLocation1": "Thanjavur, Tamil Nadu", + "demoTimePickerDescription": "Shows a dialogue containing a material design time picker.", + "demoPickersShowPicker": "SHOW PICKER", + "demoTabsScrollingTitle": "Scrolling", + "demoTabsNonScrollingTitle": "Non-scrolling", + "craneHours": "{hours,plural,=1{1 h}other{{hours}h}}", + "craneMinutes": "{minutes,plural,=1{1 m}other{{minutes}m}}", + "craneFlightDuration": "{hoursShortForm} {minutesShortForm}", + "dataTableHeader": "Nutrition", + "demoDatePickerTitle": "Date picker", + "demoPickersSubtitle": "Date and time selection", + "demoPickersTitle": "Pickers", + "demo2dTransformationsEditTooltip": "Edit tile", + "demoDataTableDescription": "Data tables display information in a grid-like format of rows and columns. They organise information in a way that's easy to scan, so that users can look for patterns and insights.", + "demo2dTransformationsDescription": "Tap to edit tiles, and use gestures to move around the scene. Drag to pan, pinch to zoom, rotate with two fingers. Press the reset button to return to the starting orientation.", + "demo2dTransformationsSubtitle": "Pan, zoom, rotate", + "demo2dTransformationsTitle": "2D transformations", + "demoCupertinoTextFieldPIN": "PIN", + "demoCupertinoTextFieldDescription": "A text field allows the user to enter text, either with a hardware keyboard or with an on-screen keyboard.", + "demoCupertinoTextFieldSubtitle": "iOS-style text fields", + "demoCupertinoTextFieldTitle": "Text fields", + "demoDatePickerDescription": "Shows a dialogue containing a material design date picker.", + "demoCupertinoPickerTime": "Time", + "demoCupertinoPickerDate": "Date", + "demoCupertinoPickerTimer": "Timer", + "demoCupertinoPickerDescription": "An iOS-style picker widget that can be used to select strings, dates, times or both date and time.", + "demoCupertinoPickerSubtitle": "iOS-style pickers", + "demoCupertinoPickerTitle": "Pickers", + "dataTableRowWithHoney": "{value} with honey", + "cardsDemoTravelDestinationCity2": "Chettinad", + "bannerDemoResetText": "Reset the banner", + "bannerDemoMultipleText": "Multiple actions", + "bannerDemoLeadingText": "Leading icon", + "dismiss": "DISMISS", + "cardsDemoTappable": "Tappable", + "cardsDemoSelectable": "Selectable (long press)", + "cardsDemoExplore": "Explore", + "cardsDemoExploreSemantics": "Explore {destinationName}", + "cardsDemoShareSemantics": "Share {destinationName}", + "cardsDemoTravelDestinationTitle1": "Top 10 cities to visit in Tamil Nadu", + "cardsDemoTravelDestinationDescription1": "Number 10", + "cardsDemoTravelDestinationCity1": "Thanjavur", + "dataTableColumnProtein": "Protein (gm)", + "cardsDemoTravelDestinationTitle2": "Artisans of Southern India", + "cardsDemoTravelDestinationDescription2": "Silk spinners", + "bannerDemoText": "Your password was updated on your other device. Please sign in again.", + "cardsDemoTravelDestinationLocation2": "Sivaganga, Tamil Nadu", + "cardsDemoTravelDestinationTitle3": "Brihadisvara Temple", + "cardsDemoTravelDestinationDescription3": "Temples", + "demoBannerTitle": "Banner", + "demoBannerSubtitle": "Displaying a banner within a list", + "demoBannerDescription": "A banner displays an important, succinct message, and provides actions for users to address (or dismiss the banner). A user action is required for it to be dismissed.", + "demoCardTitle": "Cards", + "demoCardSubtitle": "Baseline cards with rounded corners", + "demoCardDescription": "A card is a sheet of material used to represent some related information, for example, an album, a geographical location, a meal, contact details, etc.", + "demoDataTableTitle": "Data tables", + "demoDataTableSubtitle": "Rows and columns of information", + "dataTableColumnCarbs": "Carbs (gm)", + "placeTanjore": "Tanjore", + "demoGridListsTitle": "Grid lists", + "placeFlowerMarket": "Flower market", + "placeBronzeWorks": "Bronze works", + "placeMarket": "Market", + "placeThanjavurTemple": "Thanjavur Temple", + "placeSaltFarm": "Salt farm", + "placeScooters": "Scooters", + "placeSilkMaker": "Silk maker", + "placeLunchPrep": "Lunch prep", + "placeBeach": "Beach", + "placeFisherman": "Fisherman", + "demoMenuSelected": "Selected: {value}", + "demoMenuRemove": "Remove", + "demoMenuGetLink": "Get link", + "demoMenuShare": "Share", + "demoBottomAppBarSubtitle": "Displays navigation and actions at the bottom", + "demoMenuAnItemWithASectionedMenu": "An item with a sectioned menu", + "demoMenuADisabledMenuItem": "Disabled menu item", + "demoLinearProgressIndicatorTitle": "Linear progress indicator", + "demoMenuContextMenuItemOne": "Context menu item one", + "demoMenuAnItemWithASimpleMenu": "An item with a simple menu", + "demoCustomSlidersTitle": "Custom sliders", + "demoMenuAnItemWithAChecklistMenu": "An item with a checklist menu", + "demoCupertinoActivityIndicatorTitle": "Activity indicator", + "demoCupertinoActivityIndicatorSubtitle": "iOS-style activity indicators", + "demoCupertinoActivityIndicatorDescription": "An iOS-style activity indicator that spins clockwise.", + "demoCupertinoNavigationBarTitle": "Navigation bar", + "demoCupertinoNavigationBarSubtitle": "iOS-style navigation bar", + "demoCupertinoNavigationBarDescription": "An iOS-styled navigation bar. The navigation bar is a toolbar that minimally consists of a page title, in the middle of the toolbar.", + "demoCupertinoPullToRefreshTitle": "Pull to refresh", + "demoCupertinoPullToRefreshSubtitle": "iOS-style pull to refresh control", + "demoCupertinoPullToRefreshDescription": "A widget implementing the iOS-style pull to refresh content control.", + "demoProgressIndicatorTitle": "Progress indicators", + "demoProgressIndicatorSubtitle": "Linear, circular, indeterminate", + "demoCircularProgressIndicatorTitle": "Circular progress indicator", + "demoCircularProgressIndicatorDescription": "A material design circular progress indicator, which spins to indicate that the application is busy.", + "demoMenuFour": "Four", + "demoLinearProgressIndicatorDescription": "A material design linear progress indicator, also known as a progress bar.", + "demoTooltipTitle": "Tooltips", + "demoTooltipSubtitle": "Short message displayed on long press or hover", + "demoTooltipDescription": "Tooltips provide text labels that help to explain the function of a button or other user interface action. Tooltips display informative text when users hover over, focus on or long press an element.", + "demoTooltipInstructions": "Long press or hover to display the tooltip.", + "placeChennai": "Chennai", + "demoMenuChecked": "Checked: {value}", + "placeChettinad": "Chettinad", + "demoMenuPreview": "Preview", + "demoBottomAppBarTitle": "Bottom app bar", + "demoBottomAppBarDescription": "Bottom app bars provide access to a bottom navigation drawer and up to four actions, including the floating action button.", + "bottomAppBarNotch": "Notch", + "bottomAppBarPosition": "Floating action button position", + "bottomAppBarPositionDockedEnd": "Docked - End", + "bottomAppBarPositionDockedCenter": "Docked - Centre", + "bottomAppBarPositionFloatingEnd": "Floating - End", + "bottomAppBarPositionFloatingCenter": "Floating - Centre", + "demoSlidersEditableNumericalValue": "Editable numerical value", + "demoGridListsSubtitle": "Row and column layout", + "demoGridListsDescription": "Grid lists are best suited for presenting homogeneous data, typically images. Each item in a grid list is called a tile.", + "demoGridListsImageOnlyTitle": "Image only", + "demoGridListsHeaderTitle": "With header", + "demoGridListsFooterTitle": "With footer", + "demoSlidersTitle": "Sliders", + "demoSlidersSubtitle": "Widgets for selecting a value by swiping", + "demoSlidersDescription": "Sliders reflect a range of values along a bar, from which users may select a single value. They are ideal for adjusting settings such as volume, brightness or applying image filters.", + "demoRangeSlidersTitle": "Range sliders", + "demoRangeSlidersDescription": "Sliders reflect a range of values along a bar. They can have icons on both ends of the bar that reflect a range of values. They are ideal for adjusting settings such as volume, brightness or applying image filters.", + "demoMenuAnItemWithAContextMenuButton": "An item with a context menu", + "demoCustomSlidersDescription": "Sliders reflect a range of values along a bar, from which users may select a single value or range of values. The sliders can be themed and customised.", + "demoSlidersContinuousWithEditableNumericalValue": "Continuous with editable numerical value", + "demoSlidersDiscrete": "Discrete", + "demoSlidersDiscreteSliderWithCustomTheme": "Discrete slider with custom theme", + "demoSlidersContinuousRangeSliderWithCustomTheme": "Continuous range slider with custom theme", + "demoSlidersContinuous": "Continuous", + "placePondicherry": "Pondicherry", + "demoMenuTitle": "Menu", + "demoContextMenuTitle": "Context menu", + "demoSectionedMenuTitle": "Sectioned menu", + "demoSimpleMenuTitle": "Simple menu", + "demoChecklistMenuTitle": "Checklist menu", + "demoMenuSubtitle": "Menu buttons and simple menus", + "demoMenuDescription": "A menu displays a list of choices on a temporary surface. They appear when users interact with a button, action or other control.", + "demoMenuItemValueOne": "Menu item one", + "demoMenuItemValueTwo": "Menu item two", + "demoMenuItemValueThree": "Menu item three", + "demoMenuOne": "One", + "demoMenuTwo": "Two", + "demoMenuThree": "Three", + "demoMenuContextMenuItemThree": "Context menu item three", + "demoCupertinoSwitchSubtitle": "iOS-style switch", + "demoSnackbarsText": "This is a snackbar.", + "demoCupertinoSliderSubtitle": "iOS-style slider", + "demoCupertinoSliderDescription": "A slider can be used to select from either a continuous or a discrete set of values.", + "demoCupertinoSliderContinuous": "Continuous: {value}", + "demoCupertinoSliderDiscrete": "Discrete: {value}", + "demoSnackbarsAction": "You pressed the snackbar action.", + "backToGallery": "Back to Gallery", + "demoCupertinoTabBarTitle": "Tab bar", + "demoCupertinoSwitchDescription": "A switch is used to toggle the on/off state of a single setting.", + "demoSnackbarsActionButtonLabel": "ACTION", + "cupertinoTabBarProfileTab": "Profile", + "demoSnackbarsButtonLabel": "SHOW A SNACKBAR", + "demoSnackbarsDescription": "Snackbars inform users of a process that an app has performed or will perform. They appear temporarily, towards the bottom of the screen. They shouldn't interrupt the user experience, and they don't require user input to disappear.", + "demoSnackbarsSubtitle": "Snackbars show messages at the bottom of the screen", + "demoSnackbarsTitle": "Snackbars", + "demoCupertinoSliderTitle": "Slider", + "cupertinoTabBarChatTab": "Chat", + "cupertinoTabBarHomeTab": "Home", + "demoCupertinoTabBarDescription": "An iOS-style bottom navigation tab bar. Displays multiple tabs with one tab being active, the first tab by default.", + "demoCupertinoTabBarSubtitle": "iOS-style bottom tab bar", + "demoOptionsFeatureTitle": "View options", + "demoOptionsFeatureDescription": "Tap here to view available options for this demo.", + "demoCodeViewerCopyAll": "COPY ALL", + "shrineScreenReaderRemoveProductButton": "Remove {product}", + "shrineScreenReaderProductAddToCart": "Add to basket", + "shrineScreenReaderCart": "{quantity,plural,=0{Shopping basket, no items}=1{Shopping basket, 1 item}other{Shopping basket, {quantity} items}}", + "demoCodeViewerFailedToCopyToClipboardMessage": "Failed to copy to clipboard: {error}", + "demoCodeViewerCopiedToClipboardMessage": "Copied to clipboard.", + "craneSleep8SemanticLabel": "Mayan ruins on a cliff above a beach", + "craneSleep4SemanticLabel": "Lake-side hotel in front of mountains", + "craneSleep2SemanticLabel": "Machu Picchu citadel", + "craneSleep1SemanticLabel": "Chalet in a snowy landscape with evergreen trees", + "craneSleep0SemanticLabel": "Overwater bungalows", + "craneFly13SemanticLabel": "Seaside pool with palm trees", + "craneFly12SemanticLabel": "Pool with palm trees", + "craneFly11SemanticLabel": "Brick lighthouse at sea", + "craneFly10SemanticLabel": "Al-Azhar Mosque towers during sunset", + "craneFly9SemanticLabel": "Man leaning on an antique blue car", + "craneFly8SemanticLabel": "Supertree Grove", + "craneEat9SemanticLabel": "Café counter with pastries", + "craneEat2SemanticLabel": "Burger", + "craneFly5SemanticLabel": "Lake-side hotel in front of mountains", + "demoSelectionControlsSubtitle": "Tick boxes, radio buttons and switches", + "craneEat10SemanticLabel": "Woman holding huge pastrami sandwich", + "craneFly4SemanticLabel": "Overwater bungalows", + "craneEat7SemanticLabel": "Bakery entrance", + "craneEat6SemanticLabel": "Shrimp dish", + "craneEat5SemanticLabel": "Artsy restaurant seating area", + "craneEat4SemanticLabel": "Chocolate dessert", + "craneEat3SemanticLabel": "Korean taco", + "craneFly3SemanticLabel": "Machu Picchu citadel", + "craneEat1SemanticLabel": "Empty bar with diner-style stools", + "craneEat0SemanticLabel": "Pizza in a wood-fired oven", + "craneSleep11SemanticLabel": "Taipei 101 skyscraper", + "craneSleep10SemanticLabel": "Al-Azhar Mosque towers during sunset", + "craneSleep9SemanticLabel": "Brick lighthouse at sea", + "craneEat8SemanticLabel": "Plate of crawfish", + "craneSleep7SemanticLabel": "Colourful apartments at Ribeira Square", + "craneSleep6SemanticLabel": "Pool with palm trees", + "craneSleep5SemanticLabel": "Tent in a field", + "settingsButtonCloseLabel": "Close settings", + "demoSelectionControlsCheckboxDescription": "Tick boxes allow the user to select multiple options from a set. A normal tick box's value is true or false and a tristate tick box's value can also be null.", + "settingsButtonLabel": "Settings", + "demoListsTitle": "Lists", + "demoListsSubtitle": "Scrolling list layouts", + "demoListsDescription": "A single fixed-height row that typically contains some text as well as a leading or trailing icon.", + "demoOneLineListsTitle": "One line", + "demoTwoLineListsTitle": "Two lines", + "demoListsSecondary": "Secondary text", + "demoSelectionControlsTitle": "Selection controls", + "craneFly7SemanticLabel": "Mount Rushmore", + "demoSelectionControlsCheckboxTitle": "Tick box", + "craneSleep3SemanticLabel": "Man leaning on an antique blue car", + "demoSelectionControlsRadioTitle": "Radio", + "demoSelectionControlsRadioDescription": "Radio buttons allow the user to select one option from a set. Use radio buttons for exclusive selection if you think that the user needs to see all available options side by side.", + "demoSelectionControlsSwitchTitle": "Switch", + "demoSelectionControlsSwitchDescription": "On/off switches toggle the state of a single settings option. The option that the switch controls, as well as the state it's in, should be made clear from the corresponding inline label.", + "craneFly0SemanticLabel": "Chalet in a snowy landscape with evergreen trees", + "craneFly1SemanticLabel": "Tent in a field", + "craneFly2SemanticLabel": "Prayer flags in front of snowy mountain", + "craneFly6SemanticLabel": "Aerial view of Palacio de Bellas Artes", + "rallySeeAllAccounts": "See all accounts", + "rallyBillAmount": "{billName} bill due {date} for {amount}.", + "shrineTooltipCloseCart": "Close basket", + "shrineTooltipCloseMenu": "Close menu", + "shrineTooltipOpenMenu": "Open menu", + "shrineTooltipSettings": "Settings", + "shrineTooltipSearch": "Search", + "demoTabsDescription": "Tabs organise content across different screens, data sets and other interactions.", + "demoTabsSubtitle": "Tabs with independently scrollable views", + "demoTabsTitle": "Tabs", + "rallyBudgetAmount": "{budgetName} budget with {amountUsed} used of {amountTotal}, {amountLeft} left", + "shrineTooltipRemoveItem": "Remove item", + "rallyAccountAmount": "{accountName} account {accountNumber} with {amount}.", + "rallySeeAllBudgets": "See all budgets", + "rallySeeAllBills": "See all bills", + "craneFormDate": "Select date", + "craneFormOrigin": "Choose origin", + "craneFly2": "Khumbu Valley, Nepal", + "craneFly3": "Machu Picchu, Peru", + "craneFly4": "Malé, Maldives", + "craneFly5": "Vitznau, Switzerland", + "craneFly6": "Mexico City, Mexico", + "craneFly7": "Mount Rushmore, United States", + "settingsTextDirectionLocaleBased": "Based on locale", + "craneFly9": "Havana, Cuba", + "craneFly10": "Cairo, Egypt", + "craneFly11": "Lisbon, Portugal", + "craneFly12": "Napa, United States", + "craneFly13": "Bali, Indonesia", + "craneSleep0": "Malé, Maldives", + "craneSleep1": "Aspen, United States", + "craneSleep2": "Machu Picchu, Peru", + "demoCupertinoSegmentedControlTitle": "Segmented control", + "craneSleep4": "Vitznau, Switzerland", + "craneSleep5": "Big Sur, United States", + "craneSleep6": "Napa, United States", + "craneSleep7": "Porto, Portugal", + "craneSleep8": "Tulum, Mexico", + "craneEat5": "Seoul, South Korea", + "demoChipTitle": "Chips", + "demoChipSubtitle": "Compact elements that represent an input, attribute or action", + "demoActionChipTitle": "Action chip", + "demoActionChipDescription": "Action chips are a set of options which trigger an action related to primary content. Action chips should appear dynamically and contextually in a UI.", + "demoChoiceChipTitle": "Choice chip", + "demoChoiceChipDescription": "Choice chips represent a single choice from a set. Choice chips contain related descriptive text or categories.", + "demoFilterChipTitle": "Filter chip", + "demoFilterChipDescription": "Filter chips use tags or descriptive words as a way to filter content.", + "demoInputChipTitle": "Input chip", + "demoInputChipDescription": "Input chips represent a complex piece of information, such as an entity (person, place or thing) or conversational text, in a compact form.", + "craneSleep9": "Lisbon, Portugal", + "craneEat10": "Lisbon, Portugal", + "demoCupertinoSegmentedControlDescription": "Used to select between a number of mutually exclusive options. When one option in the segmented control is selected, the other options in the segmented control cease to be selected.", + "chipTurnOnLights": "Turn on lights", + "chipSmall": "Small", + "chipMedium": "Medium", + "chipLarge": "Large", + "chipElevator": "Lift", + "chipWasher": "Washing machine", + "chipFireplace": "Fireplace", + "chipBiking": "Cycling", + "craneFormDiners": "Diners", + "rallyAlertsMessageUnassignedTransactions": "{count,plural,=1{Increase your potential tax deduction! Assign categories to 1 unassigned transaction.}other{Increase your potential tax deduction! Assign categories to {count} unassigned transactions.}}", + "craneFormTime": "Select time", + "craneFormLocation": "Select location", + "craneFormTravelers": "Travellers", + "craneEat8": "Atlanta, United States", + "craneFormDestination": "Choose destination", + "craneFormDates": "Select dates", + "craneFly": "FLY", + "craneSleep": "SLEEP", + "craneEat": "EAT", + "craneFlySubhead": "Explore flights by destination", + "craneSleepSubhead": "Explore properties by destination", + "craneEatSubhead": "Explore restaurants by destination", + "craneFlyStops": "{numberOfStops,plural,=0{Non-stop}=1{1 stop}other{{numberOfStops} stops}}", + "craneSleepProperties": "{totalProperties,plural,=0{No available properties}=1{1 available property}other{{totalProperties} available properties}}", + "craneEatRestaurants": "{totalRestaurants,plural,=0{No restaurants}=1{1 restaurant}other{{totalRestaurants} restaurants}}", + "craneFly0": "Aspen, United States", + "demoCupertinoSegmentedControlSubtitle": "iOS-style segmented control", + "craneSleep10": "Cairo, Egypt", + "craneEat9": "Madrid, Spain", + "craneFly1": "Big Sur, United States", + "craneEat7": "Nashville, United States", + "craneEat6": "Seattle, United States", + "craneFly8": "Singapore", + "craneEat4": "Paris, France", + "craneEat3": "Portland, United States", + "craneEat2": "Córdoba, Argentina", + "craneEat1": "Dallas, United States", + "craneEat0": "Naples, Italy", + "craneSleep11": "Taipei, Taiwan", + "craneSleep3": "Havana, Cuba", + "shrineLogoutButtonCaption": "LOGOUT", + "rallyTitleBills": "BILLS", + "rallyTitleAccounts": "ACCOUNTS", + "shrineProductVagabondSack": "Vagabond sack", + "rallyAccountDetailDataInterestYtd": "Interest YTD", + "shrineProductWhitneyBelt": "Whitney belt", + "shrineProductGardenStrand": "Garden strand", + "shrineProductStrutEarrings": "Strut earrings", + "shrineProductVarsitySocks": "Varsity socks", + "shrineProductWeaveKeyring": "Weave keyring", + "shrineProductGatsbyHat": "Gatsby hat", + "shrineProductShrugBag": "Shrug bag", + "shrineProductGiltDeskTrio": "Gilt desk trio", + "shrineProductCopperWireRack": "Copper wire rack", + "shrineProductSootheCeramicSet": "Soothe ceramic set", + "shrineProductHurrahsTeaSet": "Hurrahs tea set", + "shrineProductBlueStoneMug": "Blue stone mug", + "shrineProductRainwaterTray": "Rainwater tray", + "shrineProductChambrayNapkins": "Chambray napkins", + "shrineProductSucculentPlanters": "Succulent planters", + "shrineProductQuartetTable": "Quartet table", + "shrineProductKitchenQuattro": "Kitchen quattro", + "shrineProductClaySweater": "Clay sweater", + "shrineProductSeaTunic": "Sea tunic", + "shrineProductPlasterTunic": "Plaster tunic", + "rallyBudgetCategoryRestaurants": "Restaurants", + "shrineProductChambrayShirt": "Chambray shirt", + "shrineProductSeabreezeSweater": "Seabreeze sweater", + "shrineProductGentryJacket": "Gentry jacket", + "shrineProductNavyTrousers": "Navy trousers", + "shrineProductWalterHenleyWhite": "Walter henley (white)", + "shrineProductSurfAndPerfShirt": "Surf and perf shirt", + "shrineProductGingerScarf": "Ginger scarf", + "shrineProductRamonaCrossover": "Ramona crossover", + "shrineProductClassicWhiteCollar": "Classic white collar", + "shrineProductSunshirtDress": "Sunshirt dress", + "rallyAccountDetailDataInterestRate": "Interest rate", + "rallyAccountDetailDataAnnualPercentageYield": "Annual percentage yield", + "rallyAccountDataVacation": "Holiday", + "shrineProductFineLinesTee": "Fine lines tee", + "rallyAccountDataHomeSavings": "Home savings", + "rallyAccountDataChecking": "Current", + "rallyAccountDetailDataInterestPaidLastYear": "Interest paid last year", + "rallyAccountDetailDataNextStatement": "Next statement", + "rallyAccountDetailDataAccountOwner": "Account owner", + "rallyBudgetCategoryCoffeeShops": "Coffee shops", + "rallyBudgetCategoryGroceries": "Groceries", + "shrineProductCeriseScallopTee": "Cerise scallop tee", + "rallyBudgetCategoryClothing": "Clothing", + "rallySettingsManageAccounts": "Manage accounts", + "rallyAccountDataCarSavings": "Car savings", + "rallySettingsTaxDocuments": "Tax documents", + "rallySettingsPasscodeAndTouchId": "Passcode and Touch ID", + "rallySettingsNotifications": "Notifications", + "rallySettingsPersonalInformation": "Personal information", + "rallySettingsPaperlessSettings": "Paperless settings", + "rallySettingsFindAtms": "Find ATMs", + "rallySettingsHelp": "Help", + "rallySettingsSignOut": "Sign out", + "rallyAccountTotal": "Total", + "rallyBillsDue": "Due", + "rallyBudgetLeft": "Left", + "rallyAccounts": "Accounts", + "rallyBills": "Bills", + "rallyBudgets": "Budgets", + "rallyAlerts": "Alerts", + "rallySeeAll": "SEE ALL", + "rallyFinanceLeft": "LEFT", + "rallyTitleOverview": "OVERVIEW", + "shrineProductShoulderRollsTee": "Shoulder rolls tee", + "shrineNextButtonCaption": "NEXT", + "rallyTitleBudgets": "BUDGETS", + "rallyTitleSettings": "SETTINGS", + "rallyLoginLoginToRally": "Log in to Rally", + "rallyLoginNoAccount": "Don't have an account?", + "rallyLoginSignUp": "SIGN UP", + "rallyLoginUsername": "Username", + "rallyLoginPassword": "Password", + "rallyLoginLabelLogin": "Log in", + "rallyLoginRememberMe": "Remember me", + "rallyLoginButtonLogin": "LOGIN", + "rallyAlertsMessageHeadsUpShopping": "Heads up: you've used up {percent} of your shopping budget for this month.", + "rallyAlertsMessageSpentOnRestaurants": "You've spent {amount} on restaurants this week.", + "rallyAlertsMessageATMFees": "You've spent {amount} in ATM fees this month", + "rallyAlertsMessageCheckingAccount": "Good work! Your current account is {percent} higher than last month.", + "shrineMenuCaption": "MENU", + "shrineCategoryNameAll": "ALL", + "shrineCategoryNameAccessories": "ACCESSORIES", + "shrineCategoryNameClothing": "CLOTHING", + "shrineCategoryNameHome": "HOME", + "shrineLoginUsernameLabel": "Username", + "shrineLoginPasswordLabel": "Password", + "shrineCancelButtonCaption": "CANCEL", + "shrineCartTaxCaption": "Tax:", + "shrineCartPageCaption": "BASKET", + "shrineProductQuantity": "Quantity: {quantity}", + "shrineProductPrice": "x {price}", + "shrineCartItemCount": "{quantity,plural,=0{NO ITEMS}=1{1 ITEM}other{{quantity} ITEMS}}", + "shrineCartClearButtonCaption": "CLEAR BASKET", + "shrineCartTotalCaption": "TOTAL", + "shrineCartSubtotalCaption": "Subtotal:", + "shrineCartShippingCaption": "Delivery:", + "shrineProductGreySlouchTank": "Grey slouch tank top", + "shrineProductStellaSunglasses": "Stella sunglasses", + "shrineProductWhitePinstripeShirt": "White pinstripe shirt", + "demoTextFieldWhereCanWeReachYou": "Where can we contact you?", + "settingsTextDirectionLTR": "LTR", + "settingsTextScalingLarge": "Large", + "demoBottomSheetHeader": "Header", + "demoBottomSheetItem": "Item {value}", + "demoBottomTextFieldsTitle": "Text fields", + "demoTextFieldTitle": "Text fields", + "demoTextFieldSubtitle": "Single line of editable text and numbers", + "demoTextFieldDescription": "Text fields allow users to enter text into a UI. They typically appear in forms and dialogues.", + "demoTextFieldShowPasswordLabel": "Show password", + "demoTextFieldHidePasswordLabel": "Hide password", + "demoTextFieldFormErrors": "Please fix the errors in red before submitting.", + "demoTextFieldNameRequired": "Name is required.", + "demoTextFieldOnlyAlphabeticalChars": "Please enter only alphabetical characters.", + "demoTextFieldEnterUSPhoneNumber": "(###) ###-#### – Enter a US phone number.", + "demoTextFieldEnterPassword": "Please enter a password.", + "demoTextFieldPasswordsDoNotMatch": "The passwords don't match", + "demoTextFieldWhatDoPeopleCallYou": "What do people call you?", + "demoTextFieldNameField": "Name*", + "demoBottomSheetButtonText": "SHOW BOTTOM SHEET", + "demoTextFieldPhoneNumber": "Phone number*", + "demoBottomSheetTitle": "Bottom sheet", + "demoTextFieldEmail": "Email", + "demoTextFieldTellUsAboutYourself": "Tell us about yourself (e.g. write down what you do or what hobbies you have)", + "demoTextFieldKeepItShort": "Keep it short, this is just a demo.", + "starterAppGenericButton": "BUTTON", + "demoTextFieldLifeStory": "Life story", + "demoTextFieldSalary": "Salary", + "demoTextFieldUSD": "USD", + "demoTextFieldNoMoreThan": "No more than 8 characters.", + "demoTextFieldPassword": "Password*", + "demoTextFieldRetypePassword": "Re-type password*", + "demoTextFieldSubmit": "SUBMIT", + "demoBottomNavigationSubtitle": "Bottom navigation with cross-fading views", + "demoBottomSheetAddLabel": "Add", + "demoBottomSheetModalDescription": "A modal bottom sheet is an alternative to a menu or a dialogue and prevents the user from interacting with the rest of the app.", + "demoBottomSheetModalTitle": "Modal bottom sheet", + "demoBottomSheetPersistentDescription": "A persistent bottom sheet shows information that supplements the primary content of the app. A persistent bottom sheet remains visible even when the user interacts with other parts of the app.", + "demoBottomSheetPersistentTitle": "Persistent bottom sheet", + "demoBottomSheetSubtitle": "Persistent and modal bottom sheets", + "demoTextFieldNameHasPhoneNumber": "{name} phone number is {phoneNumber}", + "buttonText": "BUTTON", + "demoTypographyDescription": "Definitions for the various typographical styles found in Material Design.", + "demoTypographySubtitle": "All of the predefined text styles", + "demoTypographyTitle": "Typography", + "demoFullscreenDialogDescription": "The fullscreenDialog property specifies whether the incoming page is a full-screen modal dialogue", + "demoFlatButtonDescription": "A flat button displays an ink splash on press but does not lift. Use flat buttons on toolbars, in dialogues and inline with padding", + "demoBottomNavigationDescription": "Bottom navigation bars display three to five destinations at the bottom of a screen. Each destination is represented by an icon and an optional text label. When a bottom navigation icon is tapped, the user is taken to the top-level navigation destination associated with that icon.", + "demoBottomNavigationSelectedLabel": "Selected label", + "demoBottomNavigationPersistentLabels": "Persistent labels", + "starterAppDrawerItem": "Item {value}", + "demoTextFieldRequiredField": "* indicates required field", + "demoBottomNavigationTitle": "Bottom navigation", + "settingsLightTheme": "Light", + "settingsTheme": "Theme", + "settingsPlatformIOS": "iOS", + "settingsPlatformAndroid": "Android", + "settingsTextDirectionRTL": "RTL", + "settingsTextScalingHuge": "Huge", + "cupertinoButton": "Button", + "settingsTextScalingNormal": "Normal", + "settingsTextScalingSmall": "Small", + "settingsSystemDefault": "System", + "settingsTitle": "Settings", + "rallyDescription": "A personal finance app", + "aboutDialogDescription": "To see the source code for this app, please visit the {repoLink}.", + "bottomNavigationCommentsTab": "Comments", + "starterAppGenericBody": "Body", + "starterAppGenericHeadline": "Headline", + "starterAppGenericSubtitle": "Subtitle", + "starterAppGenericTitle": "Title", + "starterAppTooltipSearch": "Search", + "starterAppTooltipShare": "Share", + "starterAppTooltipFavorite": "Favourite", + "starterAppTooltipAdd": "Add", + "bottomNavigationCalendarTab": "Calendar", + "starterAppDescription": "A responsive starter layout", + "starterAppTitle": "Starter app", + "aboutFlutterSamplesRepo": "Flutter samples GitHub repo", + "bottomNavigationContentPlaceholder": "Placeholder for {title} tab", + "bottomNavigationCameraTab": "Camera", + "bottomNavigationAlarmTab": "Alarm", + "bottomNavigationAccountTab": "Account", + "demoTextFieldYourEmailAddress": "Your email address", + "demoToggleButtonDescription": "Toggle buttons can be used to group related options. To emphasise groups of related toggle buttons, a group should share a common container", + "colorsGrey": "GREY", + "colorsBrown": "BROWN", + "colorsDeepOrange": "DEEP ORANGE", + "colorsOrange": "ORANGE", + "colorsAmber": "AMBER", + "colorsYellow": "YELLOW", + "colorsLime": "LIME", + "colorsLightGreen": "LIGHT GREEN", + "colorsGreen": "GREEN", + "homeHeaderGallery": "Gallery", + "homeHeaderCategories": "Categories", + "shrineDescription": "A fashionable retail app", + "craneDescription": "A personalised travel app", + "homeCategoryReference": "STYLES AND OTHER", + "demoInvalidURL": "Couldn't display URL:", + "demoOptionsTooltip": "Options", + "demoInfoTooltip": "Info", + "demoCodeTooltip": "Demo code", + "demoDocumentationTooltip": "API Documentation", + "demoFullscreenTooltip": "Full screen", + "settingsTextScaling": "Text scaling", + "settingsTextDirection": "Text direction", + "settingsLocale": "Locale", + "settingsPlatformMechanics": "Platform mechanics", + "settingsDarkTheme": "Dark", + "settingsSlowMotion": "Slow motion", + "settingsAbout": "About Flutter Gallery", + "settingsFeedback": "Send feedback", + "settingsAttribution": "Designed by TOASTER in London", + "demoButtonTitle": "Buttons", + "demoButtonSubtitle": "Text, elevated, outlined and more", + "demoFlatButtonTitle": "Flat Button", + "demoRaisedButtonDescription": "Raised buttons add dimension to mostly flat layouts. They emphasise functions on busy or wide spaces.", + "demoRaisedButtonTitle": "Raised Button", + "demoOutlineButtonTitle": "Outline Button", + "demoOutlineButtonDescription": "Outline buttons become opaque and elevate when pressed. They are often paired with raised buttons to indicate an alternative, secondary action.", + "demoToggleButtonTitle": "Toggle Buttons", + "colorsTeal": "TEAL", + "demoFloatingButtonTitle": "Floating Action Button", + "demoFloatingButtonDescription": "A floating action button is a circular icon button that hovers over content to promote a primary action in the application.", + "demoDialogTitle": "Dialogues", + "demoDialogSubtitle": "Simple, alert and full-screen", + "demoAlertDialogTitle": "Alert", + "demoAlertDialogDescription": "An alert dialogue informs the user about situations that require acknowledgement. An alert dialogue has an optional title and an optional list of actions.", + "demoAlertTitleDialogTitle": "Alert With Title", + "demoSimpleDialogTitle": "Simple", + "demoSimpleDialogDescription": "A simple dialogue offers the user a choice between several options. A simple dialogue has an optional title that is displayed above the choices.", + "demoFullscreenDialogTitle": "Full screen", + "demoCupertinoButtonsTitle": "Buttons", + "demoCupertinoButtonsSubtitle": "iOS-style buttons", + "demoCupertinoButtonsDescription": "An iOS-style button. It takes in text and/or an icon that fades out and in on touch. May optionally have a background.", + "demoCupertinoAlertsTitle": "Alerts", + "demoCupertinoAlertsSubtitle": "iOS-style alert dialogues", + "demoCupertinoAlertTitle": "Alert", + "demoCupertinoAlertDescription": "An alert dialogue informs the user about situations that require acknowledgement. An alert dialogue has an optional title, optional content and an optional list of actions. The title is displayed above the content and the actions are displayed below the content.", + "demoCupertinoAlertWithTitleTitle": "Alert with title", + "demoCupertinoAlertButtonsTitle": "Alert With Buttons", + "demoCupertinoAlertButtonsOnlyTitle": "Alert Buttons Only", + "demoCupertinoActionSheetTitle": "Action Sheet", + "demoCupertinoActionSheetDescription": "An action sheet is a specific style of alert that presents the user with a set of two or more choices related to the current context. An action sheet can have a title, an additional message and a list of actions.", + "demoColorsTitle": "Colours", + "demoColorsSubtitle": "All of the predefined colours", + "demoColorsDescription": "Colour and colour swatch constants which represent Material Design's colour palette.", + "buttonTextEnabled": "ENABLED", + "buttonTextDisabled": "DISABLED", + "buttonTextCreate": "Create", + "dialogSelectedOption": "You selected: '{value}'", + "dialogDiscardTitle": "Discard draft?", + "dialogLocationTitle": "Use Google's location service?", + "dialogLocationDescription": "Let Google help apps determine location. This means sending anonymous location data to Google, even when no apps are running.", + "dialogCancel": "CANCEL", + "dialogDiscard": "DISCARD", + "dialogDisagree": "DISAGREE", + "dialogAgree": "AGREE", + "dialogSetBackup": "Set backup account", + "colorsBlueGrey": "BLUE GREY", + "dialogShow": "SHOW DIALOGUE", + "dialogFullscreenTitle": "Full-Screen Dialogue", + "dialogFullscreenSave": "SAVE", + "dialogFullscreenDescription": "A full-screen dialogue demo", + "cupertinoButtonEnabled": "Enabled", + "cupertinoButtonDisabled": "Disabled", + "cupertinoButtonWithBackground": "With background", + "cupertinoAlertCancel": "Cancel", + "cupertinoAlertDiscard": "Discard", + "cupertinoAlertLocationTitle": "Allow 'Maps' to access your location while you are using the app?", + "cupertinoAlertLocationDescription": "Your current location will be displayed on the map and used for directions, nearby search results and estimated travel times.", + "cupertinoAlertAllow": "Allow", + "cupertinoAlertDontAllow": "Don't allow", + "cupertinoAlertFavoriteDessert": "Select Favourite Dessert", + "cupertinoAlertDessertDescription": "Please select your favourite type of dessert from the list below. Your selection will be used to customise the suggested list of eateries in your area.", + "cupertinoAlertCheesecake": "Cheesecake", + "cupertinoAlertTiramisu": "Tiramisu", + "cupertinoAlertApplePie": "Apple Pie", + "cupertinoAlertChocolateBrownie": "Chocolate brownie", + "cupertinoShowAlert": "Show alert", + "colorsRed": "RED", + "colorsPink": "PINK", + "colorsPurple": "PURPLE", + "colorsDeepPurple": "DEEP PURPLE", + "colorsIndigo": "INDIGO", + "colorsBlue": "BLUE", + "colorsLightBlue": "LIGHT BLUE", + "colorsCyan": "CYAN", + "dialogAddAccount": "Add account", + "Gallery": "Gallery", + "Categories": "Categories", + "SHRINE": "SHRINE", + "Basic shopping app": "Basic shopping app", + "RALLY": "RALLY", + "CRANE": "CRANE", + "Travel app": "Travel app", + "MATERIAL": "MATERIAL", + "CUPERTINO": "CUPERTINO", + "REFERENCE STYLES & MEDIA": "REFERENCE STYLES & MEDIA" +} \ No newline at end of file diff --git a/dev/integration_tests/new_gallery/lib/layout/adaptive.dart b/dev/integration_tests/new_gallery/lib/layout/adaptive.dart new file mode 100644 index 0000000000..4e198508d8 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/layout/adaptive.dart @@ -0,0 +1,44 @@ +// 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 'dart:ui'; + +import 'package:adaptive_breakpoints/adaptive_breakpoints.dart'; +import 'package:dual_screen/dual_screen.dart'; +import 'package:flutter/material.dart'; + +/// The maximum width taken up by each item on the home screen. +const double maxHomeItemWidth = 1400.0; + +/// Returns a boolean value whether the window is considered medium or large size. +/// +/// When running on a desktop device that is also foldable, the display is not +/// considered desktop. Widgets using this method might consider the display is +/// large enough for certain layouts, which is not the case on foldable devices, +/// where only part of the display is available to said widgets. +/// +/// Used to build adaptive and responsive layouts. +bool isDisplayDesktop(BuildContext context) => + !isDisplayFoldable(context) && + getWindowType(context) >= AdaptiveWindowType.medium; + +/// Returns boolean value whether the window is considered medium size. +/// +/// Used to build adaptive and responsive layouts. +bool isDisplaySmallDesktop(BuildContext context) { + return getWindowType(context) == AdaptiveWindowType.medium; +} + +/// Returns a boolean value whether the display has a hinge that splits the +/// screen into two, left and right sub-screens. Horizontal splits (top and +/// bottom sub-screens) are ignored for this application. +bool isDisplayFoldable(BuildContext context) { + final DisplayFeature? hinge = MediaQuery.of(context).hinge; + if (hinge == null) { + return false; + } else { + // Vertical + return hinge.bounds.size.aspectRatio < 1; + } +} diff --git a/dev/integration_tests/new_gallery/lib/layout/highlight_focus.dart b/dev/integration_tests/new_gallery/lib/layout/highlight_focus.dart new file mode 100644 index 0000000000..43773e65c4 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/layout/highlight_focus.dart @@ -0,0 +1,98 @@ +// 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:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// [HighlightFocus] is a helper widget for giving a child focus +/// allowing tab-navigation. +/// Wrap your widget as [child] of a [HighlightFocus] widget. +class HighlightFocus extends StatefulWidget { + const HighlightFocus({ + super.key, + required this.onPressed, + required this.child, + this.highlightColor, + this.borderColor, + this.hasFocus = true, + this.debugLabel, + }); + + /// [onPressed] is called when you press space, enter, or numpad-enter + /// when the widget is focused. + final VoidCallback onPressed; + + /// [child] is your widget. + final Widget child; + + /// [highlightColor] is the color filled in the border when the widget + /// is focused. + /// Use [Colors.transparent] if you do not want one. + /// Use an opacity less than 1 to make the underlying widget visible. + final Color? highlightColor; + + /// [borderColor] is the color of the border when the widget is focused. + final Color? borderColor; + + /// [hasFocus] is true when focusing on the widget is allowed. + /// Set to false if you want the child to skip focus. + final bool hasFocus; + + final String? debugLabel; + + @override + State createState() => _HighlightFocusState(); +} + +class _HighlightFocusState extends State { + late bool isFocused; + + @override + void initState() { + isFocused = false; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final Color highlightColor = widget.highlightColor ?? + Theme.of(context).colorScheme.primary.withOpacity(0.5); + final Color borderColor = + widget.borderColor ?? Theme.of(context).colorScheme.onPrimary; + + final BoxDecoration highlightedDecoration = BoxDecoration( + color: highlightColor, + border: Border.all( + color: borderColor, + width: 2, + strokeAlign: BorderSide.strokeAlignOutside, + ), + ); + + return Focus( + canRequestFocus: widget.hasFocus, + debugLabel: widget.debugLabel, + onFocusChange: (bool newValue) { + setState(() { + isFocused = newValue; + }); + }, + onKeyEvent: (FocusNode node, KeyEvent event) { + if ((event is KeyDownEvent || event is KeyRepeatEvent) && + (event.logicalKey == LogicalKeyboardKey.space || + event.logicalKey == LogicalKeyboardKey.enter || + event.logicalKey == LogicalKeyboardKey.numpadEnter)) { + widget.onPressed(); + return KeyEventResult.handled; + } else { + return KeyEventResult.ignored; + } + }, + child: Container( + foregroundDecoration: isFocused ? highlightedDecoration : null, + child: widget.child, + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/layout/image_placeholder.dart b/dev/integration_tests/new_gallery/lib/layout/image_placeholder.dart new file mode 100644 index 0000000000..aa3461a9e5 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/layout/image_placeholder.dart @@ -0,0 +1,75 @@ +// 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:flutter/material.dart'; + +/// An image that shows a [placeholder] widget while the target [image] is +/// loading, then fades in the new image when it loads. +/// +/// This is similar to [FadeInImage] but the difference is that it allows you +/// to specify a widget as a [placeholder], instead of just an [ImageProvider]. +/// It also lets you override the [child] argument, in case you want to wrap +/// the image with another widget, for example an [Ink.image]. +class FadeInImagePlaceholder extends StatelessWidget { + const FadeInImagePlaceholder({ + super.key, + required this.image, + required this.placeholder, + this.child, + this.duration = const Duration(milliseconds: 500), + this.excludeFromSemantics = false, + this.width, + this.height, + this.fit, + }); + + /// The target image that we are loading into memory. + final ImageProvider image; + + /// Widget displayed while the target [image] is loading. + final Widget placeholder; + + /// What widget you want to display instead of [placeholder] after [image] is + /// loaded. + /// + /// Defaults to display the [image]. + final Widget? child; + + /// The duration for how long the fade out of the placeholder and + /// fade in of [child] should take. + final Duration duration; + + /// See [Image.excludeFromSemantics]. + final bool excludeFromSemantics; + + /// See [Image.width]. + final double? width; + + /// See [Image.height]. + final double? height; + + /// See [Image.fit]. + final BoxFit? fit; + + @override + Widget build(BuildContext context) { + return Image( + image: image, + excludeFromSemantics: excludeFromSemantics, + width: width, + height: height, + fit: fit, + frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) { + if (wasSynchronouslyLoaded) { + return this.child ?? child; + } else { + return AnimatedSwitcher( + duration: duration, + child: frame != null ? this.child ?? child : placeholder, + ); + } + }, + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/layout/letter_spacing.dart b/dev/integration_tests/new_gallery/lib/layout/letter_spacing.dart new file mode 100644 index 0000000000..6c573752f0 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/layout/letter_spacing.dart @@ -0,0 +1,10 @@ +// 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:flutter/foundation.dart'; + +/// Using letter spacing in Flutter for Web can cause a performance drop, +/// see https://github.com/flutter/flutter/issues/51234. +double letterSpacingOrNone(double letterSpacing) => + kIsWeb ? 0.0 : letterSpacing; diff --git a/dev/integration_tests/new_gallery/lib/layout/text_scale.dart b/dev/integration_tests/new_gallery/lib/layout/text_scale.dart new file mode 100644 index 0000000000..20183af2ec --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/layout/text_scale.dart @@ -0,0 +1,42 @@ +// 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 'dart:math'; + +import 'package:flutter/material.dart'; + +import '../data/gallery_options.dart'; + +double _textScaleFactor(BuildContext context) { + return GalleryOptions.of(context).textScaleFactor(context); +} + +// When text is larger, this factor becomes larger, but at half the rate. +// +// | Text scaling | Text scale factor | reducedTextScale(context) | +// |--------------|-------------------|---------------------------| +// | Small | 0.8 | 1.0 | +// | Normal | 1.0 | 1.0 | +// | Large | 2.0 | 1.5 | +// | Huge | 3.0 | 2.0 | + +double reducedTextScale(BuildContext context) { + final double textScaleFactor = _textScaleFactor(context); + return textScaleFactor >= 1 ? (1 + textScaleFactor) / 2 : 1; +} + +// When text is larger, this factor becomes larger at the same rate. +// But when text is smaller, this factor stays at 1. +// +// | Text scaling | Text scale factor | cappedTextScale(context) | +// |--------------|-------------------|---------------------------| +// | Small | 0.8 | 1.0 | +// | Normal | 1.0 | 1.0 | +// | Large | 2.0 | 2.0 | +// | Huge | 3.0 | 3.0 | + +double cappedTextScale(BuildContext context) { + final double textScaleFactor = _textScaleFactor(context); + return max(textScaleFactor, 1); +} diff --git a/dev/integration_tests/new_gallery/lib/main.dart b/dev/integration_tests/new_gallery/lib/main.dart new file mode 100644 index 0000000000..223135962c --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/main.dart @@ -0,0 +1,102 @@ +// 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:dual_screen/dual_screen.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart' show timeDilation; +import 'package:flutter_localized_locales/flutter_localized_locales.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import 'constants.dart'; +import 'data/gallery_options.dart'; +import 'gallery_localizations.dart'; +import 'layout/adaptive.dart'; +import 'pages/backdrop.dart'; +import 'pages/splash.dart'; +import 'routes.dart'; +import 'themes/gallery_theme_data.dart'; + +export 'package:gallery/data/demos.dart' show pumpDeferredLibraries; + +void main() async { + GoogleFonts.config.allowRuntimeFetching = false; + runApp(const GalleryApp()); +} + +class GalleryApp extends StatelessWidget { + const GalleryApp({ + super.key, + this.initialRoute, + this.isTestMode = false, + }); + + final String? initialRoute; + final bool isTestMode; + + @override + Widget build(BuildContext context) { + return ModelBinding( + initialModel: GalleryOptions( + themeMode: ThemeMode.system, + textScaleFactor: systemTextScaleFactorOption, + customTextDirection: CustomTextDirection.localeBased, + locale: null, + timeDilation: timeDilation, + platform: defaultTargetPlatform, + isTestMode: isTestMode, + ), + child: Builder( + builder: (BuildContext context) { + final GalleryOptions options = GalleryOptions.of(context); + final bool hasHinge = MediaQuery.of(context).hinge?.bounds != null; + return MaterialApp( + restorationScopeId: 'rootGallery', + title: 'Flutter Gallery', + debugShowCheckedModeBanner: false, + themeMode: options.themeMode, + theme: GalleryThemeData.lightThemeData.copyWith( + platform: options.platform, + ), + darkTheme: GalleryThemeData.darkThemeData.copyWith( + platform: options.platform, + ), + localizationsDelegates: const >[ + ...GalleryLocalizations.localizationsDelegates, + LocaleNamesLocalizationsDelegate() + ], + initialRoute: initialRoute, + supportedLocales: GalleryLocalizations.supportedLocales, + locale: options.locale, + localeListResolutionCallback: (List? locales, Iterable supportedLocales) { + deviceLocale = locales?.first; + return basicLocaleListResolution(locales, supportedLocales); + }, + onGenerateRoute: (RouteSettings settings) => + RouteConfiguration.onGenerateRoute(settings, hasHinge), + ); + }, + ), + ); + } +} + +// ignore: unreachable_from_main +class RootPage extends StatelessWidget { + // ignore: unreachable_from_main + const RootPage({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return ApplyTextOptions( + child: SplashPage( + child: Backdrop( + isDesktop: isDisplayDesktop(context), + ), + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/pages/about.dart b/dev/integration_tests/new_gallery/lib/pages/about.dart new file mode 100644 index 0000000000..7a7b2ce119 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/pages/about.dart @@ -0,0 +1,130 @@ +// 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:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../gallery_localizations.dart'; + +void showAboutDialog({ + required BuildContext context, +}) { + showDialog( + context: context, + builder: (BuildContext context) { + return _AboutDialog(); + }, + ); +} + +Future getVersionNumber() async { + return '2.10.2+021002'; +} + +class _AboutDialog extends StatelessWidget { + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + final TextTheme textTheme = Theme.of(context).textTheme; + final TextStyle bodyTextStyle = + textTheme.bodyLarge!.apply(color: colorScheme.onPrimary); + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + const String name = 'Flutter Gallery'; // Don't need to localize. + const String legalese = '© 2021 The Flutter team'; // Don't need to localize. + final String repoText = localizations.githubRepo(name); + final String seeSource = localizations.aboutDialogDescription(repoText); + final int repoLinkIndex = seeSource.indexOf(repoText); + final int repoLinkIndexEnd = repoLinkIndex + repoText.length; + final String seeSourceFirst = seeSource.substring(0, repoLinkIndex); + final String seeSourceSecond = seeSource.substring(repoLinkIndexEnd); + + return AlertDialog( + backgroundColor: colorScheme.background, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + content: Container( + constraints: const BoxConstraints(maxWidth: 400), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + FutureBuilder( + future: getVersionNumber(), + builder: (BuildContext context, AsyncSnapshot snapshot) => SelectableText( + snapshot.hasData ? '$name ${snapshot.data}' : name, + style: textTheme.headlineMedium!.apply( + color: colorScheme.onPrimary, + ), + ), + ), + const SizedBox(height: 24), + SelectableText.rich( + TextSpan( + children: [ + TextSpan( + style: bodyTextStyle, + text: seeSourceFirst, + ), + TextSpan( + style: bodyTextStyle.copyWith( + color: colorScheme.primary, + ), + text: repoText, + recognizer: TapGestureRecognizer() + ..onTap = () async { + final Uri url = + Uri.parse('https://github.com/flutter/gallery/'); + if (await canLaunchUrl(url)) { + await launchUrl(url); + } + }, + ), + TextSpan( + style: bodyTextStyle, + text: seeSourceSecond, + ), + ], + ), + ), + const SizedBox(height: 18), + SelectableText( + legalese, + style: bodyTextStyle, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (BuildContext context) => Theme( + data: Theme.of(context).copyWith( + textTheme: Typography.material2018( + platform: Theme.of(context).platform, + ).black, + cardColor: Colors.white, + ), + child: const LicensePage( + applicationName: name, + applicationLegalese: legalese, + ), + ), + )); + }, + child: Text( + MaterialLocalizations.of(context).viewLicensesButtonLabel, + ), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: Text(MaterialLocalizations.of(context).closeButtonLabel), + ), + ], + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/pages/backdrop.dart b/dev/integration_tests/new_gallery/lib/pages/backdrop.dart new file mode 100644 index 0000000000..d39c5536f1 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/pages/backdrop.dart @@ -0,0 +1,304 @@ +// 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:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; + +import '../constants.dart'; +import '../data/gallery_options.dart'; +import '../gallery_localizations.dart'; +import '../layout/adaptive.dart'; +import 'home.dart'; +import 'settings.dart'; +import 'settings_icon/icon.dart' as settings_icon; + +const double _settingsButtonWidth = 64; +const double _settingsButtonHeightDesktop = 56; +const double _settingsButtonHeightMobile = 40; + +class Backdrop extends StatefulWidget { + const Backdrop({ + super.key, + required this.isDesktop, + this.settingsPage, + this.homePage, + }); + + final bool isDesktop; + final Widget? settingsPage; + final Widget? homePage; + + @override + State createState() => _BackdropState(); +} + +class _BackdropState extends State with TickerProviderStateMixin { + late AnimationController _settingsPanelController; + late AnimationController _iconController; + late FocusNode _settingsPageFocusNode; + late ValueNotifier _isSettingsOpenNotifier; + late Widget _settingsPage; + late Widget _homePage; + + @override + void initState() { + super.initState(); + _settingsPanelController = AnimationController( + vsync: this, + duration: widget.isDesktop + ? settingsPanelMobileAnimationDuration + : settingsPanelDesktopAnimationDuration); + _iconController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 500), + ); + _settingsPageFocusNode = FocusNode(); + _isSettingsOpenNotifier = ValueNotifier(false); + _settingsPage = widget.settingsPage ?? + SettingsPage( + animationController: _settingsPanelController, + ); + _homePage = widget.homePage ?? const HomePage(); + } + + @override + void dispose() { + _settingsPanelController.dispose(); + _iconController.dispose(); + _settingsPageFocusNode.dispose(); + _isSettingsOpenNotifier.dispose(); + super.dispose(); + } + + void _toggleSettings() { + // Animate the settings panel to open or close. + if (_isSettingsOpenNotifier.value) { + _settingsPanelController.reverse(); + _iconController.reverse(); + } else { + _settingsPanelController.forward(); + _iconController.forward(); + } + _isSettingsOpenNotifier.value = !_isSettingsOpenNotifier.value; + } + + Animation _slideDownSettingsPageAnimation( + BoxConstraints constraints) { + return RelativeRectTween( + begin: RelativeRect.fromLTRB(0, -constraints.maxHeight, 0, 0), + end: RelativeRect.fill, + ).animate( + CurvedAnimation( + parent: _settingsPanelController, + curve: const Interval( + 0.0, + 0.4, + curve: Curves.ease, + ), + ), + ); + } + + Animation _slideDownHomePageAnimation( + BoxConstraints constraints) { + return RelativeRectTween( + begin: RelativeRect.fill, + end: RelativeRect.fromLTRB( + 0, + constraints.biggest.height - galleryHeaderHeight, + 0, + -galleryHeaderHeight, + ), + ).animate( + CurvedAnimation( + parent: _settingsPanelController, + curve: const Interval( + 0.0, + 0.4, + curve: Curves.ease, + ), + ), + ); + } + + Widget _buildStack(BuildContext context, BoxConstraints constraints) { + final bool isDesktop = isDisplayDesktop(context); + + final Widget settingsPage = ValueListenableBuilder( + valueListenable: _isSettingsOpenNotifier, + builder: (BuildContext context, bool isSettingsOpen, Widget? child) { + return ExcludeSemantics( + excluding: !isSettingsOpen, + child: isSettingsOpen + ? KeyboardListener( + includeSemantics: false, + focusNode: _settingsPageFocusNode, + onKeyEvent: (KeyEvent event) { + if (event.logicalKey == LogicalKeyboardKey.escape) { + _toggleSettings(); + } + }, + child: FocusScope(child: _settingsPage), + ) + : ExcludeFocus(child: _settingsPage), + ); + }, + ); + + final Widget homePage = ValueListenableBuilder( + valueListenable: _isSettingsOpenNotifier, + builder: (BuildContext context, bool isSettingsOpen, Widget? child) { + return ExcludeSemantics( + excluding: isSettingsOpen, + child: FocusTraversalGroup(child: _homePage), + ); + }, + ); + + return AnnotatedRegion( + value: GalleryOptions.of(context).resolvedSystemUiOverlayStyle(), + child: Stack( + children: [ + if (!isDesktop) ...[ + // Slides the settings page up and down from the top of the + // screen. + PositionedTransition( + rect: _slideDownSettingsPageAnimation(constraints), + child: settingsPage, + ), + // Slides the home page up and down below the bottom of the + // screen. + PositionedTransition( + rect: _slideDownHomePageAnimation(constraints), + child: homePage, + ), + ], + if (isDesktop) ...[ + Semantics(sortKey: const OrdinalSortKey(2), child: homePage), + ValueListenableBuilder( + valueListenable: _isSettingsOpenNotifier, + builder: (BuildContext context, bool isSettingsOpen, Widget? child) { + if (isSettingsOpen) { + return ExcludeSemantics( + child: Listener( + onPointerDown: (_) => _toggleSettings(), + child: const ModalBarrier(dismissible: false), + ), + ); + } else { + return Container(); + } + }, + ), + Semantics( + sortKey: const OrdinalSortKey(3), + child: ScaleTransition( + alignment: Directionality.of(context) == TextDirection.ltr + ? Alignment.topRight + : Alignment.topLeft, + scale: CurvedAnimation( + parent: _settingsPanelController, + curve: Curves.fastOutSlowIn, + ), + child: Align( + alignment: AlignmentDirectional.topEnd, + child: Material( + elevation: 7, + clipBehavior: Clip.antiAlias, + borderRadius: BorderRadius.circular(40), + color: Theme.of(context).colorScheme.secondaryContainer, + child: Container( + constraints: const BoxConstraints( + maxHeight: 560, + maxWidth: desktopSettingsWidth, + minWidth: desktopSettingsWidth, + ), + child: settingsPage, + ), + ), + ), + ), + ), + ], + _SettingsIcon( + animationController: _iconController, + toggleSettings: _toggleSettings, + isSettingsOpenNotifier: _isSettingsOpenNotifier, + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: _buildStack, + ); + } +} + +class _SettingsIcon extends AnimatedWidget { + const _SettingsIcon({ + required this.animationController, + required this.toggleSettings, + required this.isSettingsOpenNotifier, + }) : super(listenable: animationController); + + final AnimationController animationController; + final VoidCallback toggleSettings; + final ValueNotifier isSettingsOpenNotifier; + + String _settingsSemanticLabel(bool isOpen, BuildContext context) { + return isOpen + ? GalleryLocalizations.of(context)!.settingsButtonCloseLabel + : GalleryLocalizations.of(context)!.settingsButtonLabel; + } + + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + final double safeAreaTopPadding = MediaQuery.of(context).padding.top; + + return Align( + alignment: AlignmentDirectional.topEnd, + child: Semantics( + sortKey: const OrdinalSortKey(1), + button: true, + enabled: true, + label: _settingsSemanticLabel(isSettingsOpenNotifier.value, context), + child: SizedBox( + width: _settingsButtonWidth, + height: isDesktop + ? _settingsButtonHeightDesktop + : _settingsButtonHeightMobile + safeAreaTopPadding, + child: Material( + borderRadius: const BorderRadiusDirectional.only( + bottomStart: Radius.circular(10), + ), + color: + isSettingsOpenNotifier.value & !animationController.isAnimating + ? Colors.transparent + : Theme.of(context).colorScheme.secondaryContainer, + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: () { + toggleSettings(); + SemanticsService.announce( + _settingsSemanticLabel(isSettingsOpenNotifier.value, context), + GalleryOptions.of(context).resolvedTextDirection()!, + ); + }, + child: Padding( + padding: const EdgeInsetsDirectional.only(start: 3, end: 18), + child: settings_icon.SettingsIcon(animationController.value), + ), + ), + ), + ), + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/pages/category_list_item.dart b/dev/integration_tests/new_gallery/lib/pages/category_list_item.dart new file mode 100644 index 0000000000..678cc67721 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/pages/category_list_item.dart @@ -0,0 +1,346 @@ +// 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:flutter/material.dart'; + +import '../constants.dart'; +import '../data/demos.dart'; +import '../gallery_localizations.dart'; +import '../layout/adaptive.dart'; +import 'demo.dart'; + +typedef CategoryHeaderTapCallback = void Function(bool shouldOpenList); + +class CategoryListItem extends StatefulWidget { + const CategoryListItem({ + super.key, + this.restorationId, + required this.category, + required this.imageString, + this.demos = const [], + this.initiallyExpanded = false, + this.onTap, + }); + + final GalleryDemoCategory category; + final String? restorationId; + final String imageString; + final List demos; + final bool initiallyExpanded; + final CategoryHeaderTapCallback? onTap; + + @override + State createState() => _CategoryListItemState(); +} + +class _CategoryListItemState extends State + with SingleTickerProviderStateMixin { + static final Animatable _easeInTween = + CurveTween(curve: Curves.easeIn); + static const Duration _expandDuration = Duration(milliseconds: 200); + late AnimationController _controller; + late Animation _childrenHeightFactor; + late Animation _headerChevronOpacity; + late Animation _headerHeight; + late Animation _headerMargin; + late Animation _headerImagePadding; + late Animation _childrenPadding; + late Animation _headerBorderRadius; + + @override + void initState() { + super.initState(); + + _controller = AnimationController(duration: _expandDuration, vsync: this); + _controller.addStatusListener((AnimationStatus status) { + setState(() {}); + }); + + _childrenHeightFactor = _controller.drive(_easeInTween); + _headerChevronOpacity = _controller.drive(_easeInTween); + _headerHeight = Tween( + begin: 80, + end: 96, + ).animate(_controller); + _headerMargin = EdgeInsetsGeometryTween( + begin: const EdgeInsets.fromLTRB(32, 8, 32, 8), + end: EdgeInsets.zero, + ).animate(_controller); + _headerImagePadding = EdgeInsetsGeometryTween( + begin: const EdgeInsets.all(8), + end: const EdgeInsetsDirectional.fromSTEB(16, 8, 8, 8), + ).animate(_controller); + _childrenPadding = EdgeInsetsGeometryTween( + begin: const EdgeInsets.symmetric(horizontal: 32), + end: EdgeInsets.zero, + ).animate(_controller); + _headerBorderRadius = BorderRadiusTween( + begin: BorderRadius.circular(10), + end: BorderRadius.zero, + ).animate(_controller); + + if (widget.initiallyExpanded) { + _controller.value = 1.0; + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + bool _shouldOpenList() { + switch (_controller.status) { + case AnimationStatus.completed: + case AnimationStatus.forward: + case AnimationStatus.reverse: + return false; + case AnimationStatus.dismissed: + return true; + } + } + + void _handleTap() { + if (_shouldOpenList()) { + _controller.forward(); + if (widget.onTap != null) { + widget.onTap!(true); + } + } else { + _controller.reverse(); + if (widget.onTap != null) { + widget.onTap!(false); + } + } + } + + Widget _buildHeaderWithChildren(BuildContext context, Widget? child) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + _CategoryHeader( + margin: _headerMargin.value, + imagePadding: _headerImagePadding.value, + borderRadius: _headerBorderRadius.value!, + height: _headerHeight.value, + chevronOpacity: _headerChevronOpacity.value, + imageString: widget.imageString, + category: widget.category, + onTap: _handleTap, + ), + Padding( + padding: _childrenPadding.value, + child: ClipRect( + child: Align( + heightFactor: _childrenHeightFactor.value, + child: child, + ), + ), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller.view, + builder: _buildHeaderWithChildren, + child: _shouldOpenList() + ? null + : _ExpandedCategoryDemos( + category: widget.category, + demos: widget.demos, + ), + ); + } +} + +class _CategoryHeader extends StatelessWidget { + const _CategoryHeader({ + this.margin, + required this.imagePadding, + required this.borderRadius, + this.height, + required this.chevronOpacity, + required this.imageString, + required this.category, + this.onTap, + }); + + final EdgeInsetsGeometry? margin; + final EdgeInsetsGeometry imagePadding; + final double? height; + final BorderRadiusGeometry borderRadius; + final String imageString; + final GalleryDemoCategory category; + final double chevronOpacity; + final GestureTapCallback? onTap; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + return Container( + margin: margin, + child: Material( + shape: RoundedRectangleBorder(borderRadius: borderRadius), + color: colorScheme.onBackground, + clipBehavior: Clip.antiAlias, + child: SizedBox( + width: MediaQuery.of(context).size.width, + child: InkWell( + // Makes integration tests possible. + key: ValueKey('${category.name}CategoryHeader'), + onTap: onTap, + child: Row( + children: [ + Expanded( + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Padding( + padding: imagePadding, + child: FadeInImage( + image: AssetImage( + imageString, + package: 'flutter_gallery_assets', + ), + placeholder: MemoryImage(kTransparentImage), + fadeInDuration: entranceAnimationDuration, + width: 64, + height: 64, + excludeFromSemantics: true, + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only(start: 8), + child: Text( + category.displayTitle( + GalleryLocalizations.of(context)!, + )!, + style: + Theme.of(context).textTheme.headlineSmall!.apply( + color: colorScheme.onSurface, + ), + ), + ), + ], + ), + ), + Opacity( + opacity: chevronOpacity, + child: chevronOpacity != 0 + ? Padding( + padding: const EdgeInsetsDirectional.only( + start: 8, + end: 32, + ), + child: Icon( + Icons.keyboard_arrow_up, + color: colorScheme.onSurface, + ), + ) + : null, + ), + ], + ), + ), + ), + ), + ); + } +} + +class _ExpandedCategoryDemos extends StatelessWidget { + const _ExpandedCategoryDemos({ + required this.category, + required this.demos, + }); + + final GalleryDemoCategory category; + final List demos; + + @override + Widget build(BuildContext context) { + return Column( + // Makes integration tests possible. + key: ValueKey('${category.name}DemoList'), + children: [ + for (final GalleryDemo demo in demos) + CategoryDemoItem( + demo: demo, + ), + const SizedBox(height: 12), // Extra space below. + ], + ); + } +} + +class CategoryDemoItem extends StatelessWidget { + const CategoryDemoItem({super.key, required this.demo}); + + final GalleryDemo demo; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + final ColorScheme colorScheme = Theme.of(context).colorScheme; + return Material( + // Makes integration tests possible. + key: ValueKey(demo.describe), + color: Theme.of(context).colorScheme.surface, + child: MergeSemantics( + child: InkWell( + onTap: () { + Navigator.of(context).restorablePushNamed( + '${DemoPage.baseRoute}/${demo.slug}', + ); + }, + child: Padding( + padding: EdgeInsetsDirectional.only( + start: 32, + top: 20, + end: isDisplayDesktop(context) ? 16 : 8, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + demo.icon, + color: colorScheme.primary, + ), + const SizedBox(width: 40), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + demo.title, + style: textTheme.titleMedium! + .apply(color: colorScheme.onSurface), + ), + Text( + demo.subtitle, + style: textTheme.labelSmall!.apply( + color: colorScheme.onSurface.withOpacity(0.5), + ), + ), + const SizedBox(height: 20), + Divider( + thickness: 1, + height: 1, + color: Theme.of(context).colorScheme.background, + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/pages/demo.dart b/dev/integration_tests/new_gallery/lib/pages/demo.dart new file mode 100644 index 0000000000..ee9bf74400 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/pages/demo.dart @@ -0,0 +1,819 @@ +// 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:dual_screen/dual_screen.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +import '../codeviewer/code_displayer.dart'; +import '../codeviewer/code_style.dart'; +import '../constants.dart'; +import '../data/demos.dart'; +import '../data/gallery_options.dart'; +import '../feature_discovery/feature_discovery.dart'; +import '../gallery_localizations.dart'; +import '../layout/adaptive.dart'; +import '../themes/gallery_theme_data.dart'; +import '../themes/material_demo_theme_data.dart'; +import 'splash.dart'; + +enum _DemoState { + normal, + options, + info, + code, + fullscreen, +} + +class DemoPage extends StatefulWidget { + const DemoPage({ + super.key, + required this.slug, + }); + + static const String baseRoute = '/demo'; + final String? slug; + + @override + State createState() => _DemoPageState(); +} + +class _DemoPageState extends State { + late Map slugToDemoMap; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // To make sure that we do not rebuild the map for every update to the demo + // page, we save it in a variable. The cost of running `slugToDemo` is + // still only close to constant, as it's just iterating over all of the + // demos. + slugToDemoMap = Demos.asSlugToDemoMap(context); + } + + @override + Widget build(BuildContext context) { + if (widget.slug == null || !slugToDemoMap.containsKey(widget.slug)) { + // Return to root if invalid slug. + Navigator.of(context).pop(); + } + return ScaffoldMessenger( + child: GalleryDemoPage( + restorationId: widget.slug!, + demo: slugToDemoMap[widget.slug]!, + )); + } +} + +class GalleryDemoPage extends StatefulWidget { + const GalleryDemoPage({ + super.key, + required this.restorationId, + required this.demo, + }); + + final String restorationId; + final GalleryDemo demo; + + @override + State createState() => _GalleryDemoPageState(); +} + +class _GalleryDemoPageState extends State + with RestorationMixin, TickerProviderStateMixin { + final RestorableInt _demoStateIndex = RestorableInt(_DemoState.normal.index); + final RestorableInt _configIndex = RestorableInt(0); + + bool? _isDesktop; + + late AnimationController _codeBackgroundColorController; + + @override + String get restorationId => widget.restorationId; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_demoStateIndex, 'demo_state'); + registerForRestoration(_configIndex, 'configuration_index'); + } + + GalleryDemoConfiguration get _currentConfig { + return widget.demo.configurations[_configIndex.value]; + } + + bool get _hasOptions => widget.demo.configurations.length > 1; + + @override + void initState() { + super.initState(); + _codeBackgroundColorController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + ); + } + + @override + void dispose() { + _demoStateIndex.dispose(); + _configIndex.dispose(); + _codeBackgroundColorController.dispose(); + super.dispose(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _isDesktop ??= isDisplayDesktop(context); + } + + /// Sets state and updates the background color for code. + void setStateAndUpdate(VoidCallback callback) { + setState(() { + callback(); + if (_demoStateIndex.value == _DemoState.code.index) { + _codeBackgroundColorController.forward(); + } else { + _codeBackgroundColorController.reverse(); + } + }); + } + + void _handleTap(_DemoState newState) { + final int newStateIndex = newState.index; + + // Do not allow normal state for desktop. + if (_demoStateIndex.value == newStateIndex && isDisplayDesktop(context)) { + if (_demoStateIndex.value == _DemoState.fullscreen.index) { + setStateAndUpdate(() { + _demoStateIndex.value = + _hasOptions ? _DemoState.options.index : _DemoState.info.index; + }); + } + return; + } + + setStateAndUpdate(() { + _demoStateIndex.value = _demoStateIndex.value == newStateIndex + ? _DemoState.normal.index + : newStateIndex; + }); + } + + Future _showDocumentation(BuildContext context) async { + final String url = _currentConfig.documentationUrl; + + if (await canLaunchUrlString(url)) { + await launchUrlString(url); + } else if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) { + return SimpleDialog( + title: Text(GalleryLocalizations.of(context)!.demoInvalidURL), + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text(url), + ), + ], + ); + }, + ); + } + } + + void _resolveState(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + final bool isFoldable = isDisplayFoldable(context); + if (_DemoState.values[_demoStateIndex.value] == _DemoState.fullscreen && + !isDesktop) { + // Do not allow fullscreen state for mobile. + _demoStateIndex.value = _DemoState.normal.index; + } else if (_DemoState.values[_demoStateIndex.value] == _DemoState.normal && + (isDesktop || isFoldable)) { + // Do not allow normal state for desktop. + _demoStateIndex.value = + _hasOptions ? _DemoState.options.index : _DemoState.info.index; + } else if (isDesktop != _isDesktop) { + _isDesktop = isDesktop; + // When going from desktop to mobile, return to normal state. + if (!isDesktop) { + _demoStateIndex.value = _DemoState.normal.index; + } + } + } + + @override + Widget build(BuildContext context) { + final bool isFoldable = isDisplayFoldable(context); + final bool isDesktop = isDisplayDesktop(context); + _resolveState(context); + + final ColorScheme colorScheme = Theme.of(context).colorScheme; + final Color iconColor = colorScheme.onSurface; + final Color selectedIconColor = colorScheme.primary; + final double appBarPadding = isDesktop ? 20.0 : 0.0; + final _DemoState currentDemoState = _DemoState.values[_demoStateIndex.value]; + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + final GalleryOptions options = GalleryOptions.of(context); + + final AppBar appBar = AppBar( + systemOverlayStyle: options.resolvedSystemUiOverlayStyle(), + backgroundColor: Colors.transparent, + leading: Padding( + padding: EdgeInsetsDirectional.only(start: appBarPadding), + child: IconButton( + key: const ValueKey('Back'), + icon: const BackButtonIcon(), + tooltip: MaterialLocalizations.of(context).backButtonTooltip, + onPressed: () { + Navigator.maybePop(context); + }, + ), + ), + actions: [ + if (_hasOptions) + IconButton( + icon: FeatureDiscovery( + title: localizations.demoOptionsFeatureTitle, + description: localizations.demoOptionsFeatureDescription, + showOverlay: !isDisplayDesktop(context) && !options.isTestMode, + color: colorScheme.primary, + onTap: () => _handleTap(_DemoState.options), + child: Icon( + Icons.tune, + color: currentDemoState == _DemoState.options + ? selectedIconColor + : iconColor, + ), + ), + tooltip: localizations.demoOptionsTooltip, + onPressed: () => _handleTap(_DemoState.options), + ), + IconButton( + icon: const Icon(Icons.info), + tooltip: localizations.demoInfoTooltip, + color: currentDemoState == _DemoState.info + ? selectedIconColor + : iconColor, + onPressed: () => _handleTap(_DemoState.info), + ), + IconButton( + icon: const Icon(Icons.code), + tooltip: localizations.demoCodeTooltip, + color: currentDemoState == _DemoState.code + ? selectedIconColor + : iconColor, + onPressed: () => _handleTap(_DemoState.code), + ), + IconButton( + icon: const Icon(Icons.library_books), + tooltip: localizations.demoDocumentationTooltip, + color: iconColor, + onPressed: () => _showDocumentation(context), + ), + if (isDesktop) + IconButton( + icon: const Icon(Icons.fullscreen), + tooltip: localizations.demoFullscreenTooltip, + color: currentDemoState == _DemoState.fullscreen + ? selectedIconColor + : iconColor, + onPressed: () => _handleTap(_DemoState.fullscreen), + ), + SizedBox(width: appBarPadding), + ], + ); + + final MediaQueryData mediaQuery = MediaQuery.of(context); + final double bottomSafeArea = mediaQuery.padding.bottom; + final double contentHeight = mediaQuery.size.height - + mediaQuery.padding.top - + mediaQuery.padding.bottom - + appBar.preferredSize.height; + final double maxSectionHeight = isDesktop ? contentHeight : contentHeight - 64; + final double horizontalPadding = isDesktop ? mediaQuery.size.width * 0.12 : 0.0; + const double maxSectionWidth = 420.0; + + Widget section; + switch (currentDemoState) { + case _DemoState.options: + section = _DemoSectionOptions( + maxHeight: maxSectionHeight, + maxWidth: maxSectionWidth, + configurations: widget.demo.configurations, + configIndex: _configIndex.value, + onConfigChanged: (int index) { + setStateAndUpdate(() { + _configIndex.value = index; + if (!isDesktop) { + _demoStateIndex.value = _DemoState.normal.index; + } + }); + }, + ); + case _DemoState.info: + section = _DemoSectionInfo( + maxHeight: maxSectionHeight, + maxWidth: maxSectionWidth, + title: _currentConfig.title, + description: _currentConfig.description, + ); + case _DemoState.code: + final TextStyle codeTheme = GoogleFonts.robotoMono( + fontSize: 12 * options.textScaleFactor(context), + ); + section = CodeStyle( + baseStyle: codeTheme.copyWith(color: const Color(0xFFFAFBFB)), + numberStyle: codeTheme.copyWith(color: const Color(0xFFBD93F9)), + commentStyle: codeTheme.copyWith(color: const Color(0xFF808080)), + keywordStyle: codeTheme.copyWith(color: const Color(0xFF1CDEC9)), + stringStyle: codeTheme.copyWith(color: const Color(0xFFFFA65C)), + punctuationStyle: codeTheme.copyWith(color: const Color(0xFF8BE9FD)), + classStyle: codeTheme.copyWith(color: const Color(0xFFD65BAD)), + constantStyle: codeTheme.copyWith(color: const Color(0xFFFF8383)), + child: _DemoSectionCode( + maxHeight: maxSectionHeight, + codeWidget: CodeDisplayPage( + _currentConfig.code, + ), + ), + ); + case _DemoState.normal: + case _DemoState.fullscreen: + section = Container(); + } + + Widget body; + Widget demoContent = ScaffoldMessenger( + child: DemoWrapper( + height: contentHeight, + buildRoute: _currentConfig.buildRoute, + ), + ); + if (isDesktop) { + final bool isFullScreen = currentDemoState == _DemoState.fullscreen; + final Widget sectionAndDemo = Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isFullScreen) Expanded(child: section), + SizedBox(width: !isFullScreen ? 48.0 : 0), + Expanded(child: demoContent), + ], + ); + + body = SafeArea( + child: Padding( + padding: const EdgeInsets.only(top: 56), + child: sectionAndDemo, + ), + ); + } else if (isFoldable) { + body = Padding( + padding: const EdgeInsets.only(top: 12.0), + child: TwoPane( + startPane: demoContent, + endPane: section, + ), + ); + } else { + section = AnimatedSize( + duration: const Duration(milliseconds: 200), + alignment: Alignment.topCenter, + curve: Curves.easeIn, + child: section, + ); + + final bool isDemoNormal = currentDemoState == _DemoState.normal; + // Add a tap gesture to collapse the currently opened section. + demoContent = Semantics( + label: + '${GalleryLocalizations.of(context)!.demo}, ${widget.demo.title}', + child: MouseRegion( + cursor: isDemoNormal ? MouseCursor.defer : SystemMouseCursors.click, + child: GestureDetector( + onTap: isDemoNormal + ? null + : () { + setStateAndUpdate(() { + _demoStateIndex.value = _DemoState.normal.index; + }); + }, + child: Semantics( + excludeSemantics: !isDemoNormal, + child: demoContent, + ), + ), + ), + ); + + body = SafeArea( + bottom: false, + child: ListView( + // Use a non-scrollable ListView to enable animation of shifting the + // demo offscreen. + physics: const NeverScrollableScrollPhysics(), + children: [ + section, + demoContent, + // Fake the safe area to ensure the animation looks correct. + SizedBox(height: bottomSafeArea), + ], + ), + ); + } + + Widget page; + + if (isDesktop || isFoldable) { + page = AnimatedBuilder( + animation: _codeBackgroundColorController, + builder: (BuildContext context, Widget? child) { + Brightness themeBrightness; + + switch (GalleryOptions.of(context).themeMode) { + case ThemeMode.system: + themeBrightness = MediaQuery.of(context).platformBrightness; + case ThemeMode.light: + themeBrightness = Brightness.light; + case ThemeMode.dark: + themeBrightness = Brightness.dark; + } + + Widget contents = Container( + padding: EdgeInsets.symmetric(horizontal: horizontalPadding), + child: ApplyTextOptions( + child: Scaffold( + appBar: appBar, + body: body, + backgroundColor: Colors.transparent, + ), + ), + ); + + if (themeBrightness == Brightness.light) { + // If it is currently in light mode, add a + // dark background for code. + final Widget codeBackground = SafeArea( + child: Container( + padding: const EdgeInsets.only(top: 56), + child: Container( + color: ColorTween( + begin: Colors.transparent, + end: GalleryThemeData.darkThemeData.canvasColor, + ).animate(_codeBackgroundColorController).value, + ), + ), + ); + + contents = Stack( + children: [ + codeBackground, + contents, + ], + ); + } + + return ColoredBox( + color: colorScheme.background, + child: contents, + ); + }); + } else { + page = ColoredBox( + color: colorScheme.background, + child: ApplyTextOptions( + child: Scaffold( + appBar: appBar, + body: body, + resizeToAvoidBottomInset: false, + ), + ), + ); + } + + // Add the splash page functionality for desktop. + if (isDesktop) { + page = MediaQuery.removePadding( + removeTop: true, + context: context, + child: SplashPage( + child: page, + ), + ); + } + + return FeatureDiscoveryController(page); + } +} + +class _DemoSectionOptions extends StatelessWidget { + const _DemoSectionOptions({ + required this.maxHeight, + required this.maxWidth, + required this.configurations, + required this.configIndex, + required this.onConfigChanged, + }); + + final double maxHeight; + final double maxWidth; + final List configurations; + final int configIndex; + final ValueChanged onConfigChanged; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + final ColorScheme colorScheme = Theme.of(context).colorScheme; + + return Align( + alignment: AlignmentDirectional.topStart, + child: Container( + constraints: BoxConstraints(maxHeight: maxHeight, maxWidth: maxWidth), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsetsDirectional.only( + start: 24, + top: 12, + end: 24, + ), + child: Text( + GalleryLocalizations.of(context)!.demoOptionsTooltip, + style: textTheme.headlineMedium!.apply( + color: colorScheme.onSurface, + fontSizeDelta: + isDisplayDesktop(context) ? desktopDisplay1FontDelta : 0, + ), + ), + ), + Divider( + thickness: 1, + height: 16, + color: colorScheme.onSurface, + ), + Flexible( + child: ListView( + shrinkWrap: true, + children: [ + for (final GalleryDemoConfiguration configuration in configurations) + _DemoSectionOptionsItem( + title: configuration.title, + isSelected: configuration == configurations[configIndex], + onTap: () { + onConfigChanged(configurations.indexOf(configuration)); + }, + ), + ], + ), + ), + const SizedBox(height: 12), + ], + ), + ), + ); + } +} + +class _DemoSectionOptionsItem extends StatelessWidget { + const _DemoSectionOptionsItem({ + required this.title, + required this.isSelected, + this.onTap, + }); + + final String title; + final bool isSelected; + final GestureTapCallback? onTap; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + + return Material( + color: isSelected ? colorScheme.surface : null, + child: InkWell( + onTap: onTap, + child: Container( + constraints: const BoxConstraints(minWidth: double.infinity), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), + child: Text( + title, + style: Theme.of(context).textTheme.bodyMedium!.apply( + color: + isSelected ? colorScheme.primary : colorScheme.onSurface, + ), + ), + ), + ), + ); + } +} + +class _DemoSectionInfo extends StatelessWidget { + const _DemoSectionInfo({ + required this.maxHeight, + required this.maxWidth, + required this.title, + required this.description, + }); + + final double maxHeight; + final double maxWidth; + final String title; + final String description; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + final ColorScheme colorScheme = Theme.of(context).colorScheme; + + return Align( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: const EdgeInsetsDirectional.only( + start: 24, + top: 12, + end: 24, + bottom: 32, + ), + constraints: BoxConstraints(maxHeight: maxHeight, maxWidth: maxWidth), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + SelectableText( + title, + style: textTheme.headlineMedium!.apply( + color: colorScheme.onSurface, + fontSizeDelta: + isDisplayDesktop(context) ? desktopDisplay1FontDelta : 0, + ), + ), + const SizedBox(height: 12), + SelectableText( + description, + style: textTheme.bodyMedium!.apply( + color: colorScheme.onSurface, + ), + ), + ], + ), + ), + ), + ); + } +} + +class DemoWrapper extends StatelessWidget { + const DemoWrapper({ + super.key, + required this.height, + required this.buildRoute, + }); + + final double height; + final WidgetBuilder buildRoute; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), + height: height, + child: ClipRRect( + clipBehavior: Clip.antiAliasWithSaveLayer, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(10.0), + bottom: Radius.circular(2.0), + ), + child: Theme( + data: MaterialDemoThemeData.themeData.copyWith( + platform: GalleryOptions.of(context).platform, + ), + child: CupertinoTheme( + data: const CupertinoThemeData() + .copyWith(brightness: Brightness.light), + child: ApplyTextOptions( + child: Builder(builder: buildRoute), + ), + ), + ), + ), + ); + } +} + +class _DemoSectionCode extends StatelessWidget { + const _DemoSectionCode({ + this.maxHeight, + this.codeWidget, + }); + + final double? maxHeight; + final Widget? codeWidget; + + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + + return Theme( + data: GalleryThemeData.darkThemeData, + child: Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Container( + color: isDesktop ? null : GalleryThemeData.darkThemeData.canvasColor, + padding: const EdgeInsets.symmetric(horizontal: 16), + height: maxHeight, + child: codeWidget, + ), + ), + ); + } +} + +class CodeDisplayPage extends StatelessWidget { + const CodeDisplayPage(this.code, {super.key}); + + final CodeDisplayer code; + + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + + final TextSpan richTextCode = code(context); + final String plainTextCode = richTextCode.toPlainText(); + + void showSnackBarOnCopySuccess(dynamic result) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + GalleryLocalizations.of(context)! + .demoCodeViewerCopiedToClipboardMessage, + ), + ), + ); + } + + void showSnackBarOnCopyFailure(Object exception) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + GalleryLocalizations.of(context)! + .demoCodeViewerFailedToCopyToClipboardMessage(exception), + ), + ), + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: isDesktop + ? const EdgeInsets.only(bottom: 8) + : const EdgeInsets.symmetric(vertical: 8), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white.withOpacity(0.15), + padding: const EdgeInsets.symmetric(horizontal: 8), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(4)), + ), + ), + onPressed: () async { + await Clipboard.setData(ClipboardData(text: plainTextCode)) + .then(showSnackBarOnCopySuccess) + .catchError(showSnackBarOnCopyFailure); + }, + child: Text( + GalleryLocalizations.of(context)!.demoCodeViewerCopyAll, + style: Theme.of(context).textTheme.labelLarge!.copyWith( + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + Expanded( + child: SingleChildScrollView( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8), + child: SelectableText.rich( + richTextCode, + textDirection: TextDirection.ltr, + ), + ), + ), + ), + ], + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/pages/home.dart b/dev/integration_tests/new_gallery/lib/pages/home.dart new file mode 100644 index 0000000000..3f160ae6e5 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/pages/home.dart @@ -0,0 +1,1212 @@ +// 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 'dart:async'; +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../constants.dart'; +import '../data/demos.dart'; +import '../data/gallery_options.dart'; +import '../gallery_localizations.dart'; +import '../layout/adaptive.dart'; +import '../studies/crane/colors.dart'; +import '../studies/crane/routes.dart' as crane_routes; +import '../studies/fortnightly/routes.dart' as fortnightly_routes; +import '../studies/rally/colors.dart'; +import '../studies/rally/routes.dart' as rally_routes; +import '../studies/reply/routes.dart' as reply_routes; +import '../studies/shrine/colors.dart'; +import '../studies/shrine/routes.dart' as shrine_routes; +import '../studies/starter/routes.dart' as starter_app_routes; +import 'category_list_item.dart'; +import 'settings.dart'; +import 'splash.dart'; + +const double _horizontalPadding = 32.0; +const double _horizontalDesktopPadding = 81.0; +const double _carouselHeightMin = 240.0; +const double _carouselItemDesktopMargin = 8.0; +const double _carouselItemMobileMargin = 4.0; +const double _carouselItemWidth = 296.0; + +class ToggleSplashNotification extends Notification {} + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + final Map studyDemos = Demos.studies(localizations); + final List carouselCards = [ + _CarouselCard( + demo: studyDemos['reply'], + asset: const AssetImage( + 'assets/studies/reply_card.png', + package: 'flutter_gallery_assets', + ), + assetColor: const Color(0xFF344955), + assetDark: const AssetImage( + 'assets/studies/reply_card_dark.png', + package: 'flutter_gallery_assets', + ), + assetDarkColor: const Color(0xFF1D2327), + textColor: Colors.white, + studyRoute: reply_routes.homeRoute, + ), + _CarouselCard( + demo: studyDemos['shrine'], + asset: const AssetImage( + 'assets/studies/shrine_card.png', + package: 'flutter_gallery_assets', + ), + assetColor: const Color(0xFFFEDBD0), + assetDark: const AssetImage( + 'assets/studies/shrine_card_dark.png', + package: 'flutter_gallery_assets', + ), + assetDarkColor: const Color(0xFF543B3C), + textColor: shrineBrown900, + studyRoute: shrine_routes.loginRoute, + ), + _CarouselCard( + demo: studyDemos['rally'], + textColor: RallyColors.accountColors[0], + asset: const AssetImage( + 'assets/studies/rally_card.png', + package: 'flutter_gallery_assets', + ), + assetColor: const Color(0xFFD1F2E6), + assetDark: const AssetImage( + 'assets/studies/rally_card_dark.png', + package: 'flutter_gallery_assets', + ), + assetDarkColor: const Color(0xFF253538), + studyRoute: rally_routes.loginRoute, + ), + _CarouselCard( + demo: studyDemos['crane'], + asset: const AssetImage( + 'assets/studies/crane_card.png', + package: 'flutter_gallery_assets', + ), + assetColor: const Color(0xFFFBF6F8), + assetDark: const AssetImage( + 'assets/studies/crane_card_dark.png', + package: 'flutter_gallery_assets', + ), + assetDarkColor: const Color(0xFF591946), + textColor: cranePurple700, + studyRoute: crane_routes.defaultRoute, + ), + _CarouselCard( + demo: studyDemos['fortnightly'], + asset: const AssetImage( + 'assets/studies/fortnightly_card.png', + package: 'flutter_gallery_assets', + ), + assetColor: Colors.white, + assetDark: const AssetImage( + 'assets/studies/fortnightly_card_dark.png', + package: 'flutter_gallery_assets', + ), + assetDarkColor: const Color(0xFF1F1F1F), + studyRoute: fortnightly_routes.defaultRoute, + ), + _CarouselCard( + demo: studyDemos['starterApp'], + asset: const AssetImage( + 'assets/studies/starter_card.png', + package: 'flutter_gallery_assets', + ), + assetColor: const Color(0xFFFAF6FE), + assetDark: const AssetImage( + 'assets/studies/starter_card_dark.png', + package: 'flutter_gallery_assets', + ), + assetDarkColor: const Color(0xFF3F3D45), + textColor: Colors.black, + studyRoute: starter_app_routes.defaultRoute, + ), + ]; + + if (isDesktop) { + // Desktop layout + final List<_DesktopCategoryItem> desktopCategoryItems = <_DesktopCategoryItem>[ + _DesktopCategoryItem( + category: GalleryDemoCategory.material, + asset: const AssetImage( + 'assets/icons/material/material.png', + package: 'flutter_gallery_assets', + ), + demos: Demos.materialDemos(localizations), + ), + _DesktopCategoryItem( + category: GalleryDemoCategory.cupertino, + asset: const AssetImage( + 'assets/icons/cupertino/cupertino.png', + package: 'flutter_gallery_assets', + ), + demos: Demos.cupertinoDemos(localizations), + ), + _DesktopCategoryItem( + category: GalleryDemoCategory.other, + asset: const AssetImage( + 'assets/icons/reference/reference.png', + package: 'flutter_gallery_assets', + ), + demos: Demos.otherDemos(localizations), + ), + ]; + + return Scaffold( + body: ListView( + // Makes integration tests possible. + key: const ValueKey('HomeListView'), + primary: true, + padding: const EdgeInsetsDirectional.only( + top: firstHeaderDesktopTopPadding, + ), + children: [ + _DesktopHomeItem(child: _GalleryHeader()), + _DesktopCarousel( + height: _carouselHeight(0.7, context), + children: carouselCards, + ), + _DesktopHomeItem(child: _CategoriesHeader()), + SizedBox( + height: 585, + child: _DesktopHomeItem( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: spaceBetween(28, desktopCategoryItems), + ), + ), + ), + const SizedBox(height: 81), + _DesktopHomeItem( + child: Row( + children: [ + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () async { + final Uri url = Uri.parse('https://flutter.dev'); + if (await canLaunchUrl(url)) { + await launchUrl(url); + } + }, + excludeFromSemantics: true, + child: FadeInImage( + image: Theme.of(context).colorScheme.brightness == + Brightness.dark + ? const AssetImage( + 'assets/logo/flutter_logo.png', + package: 'flutter_gallery_assets', + ) + : const AssetImage( + 'assets/logo/flutter_logo_color.png', + package: 'flutter_gallery_assets', + ), + placeholder: MemoryImage(kTransparentImage), + fadeInDuration: entranceAnimationDuration, + ), + ), + ), + const Expanded( + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + alignment: WrapAlignment.end, + children: [ + SettingsAbout(), + SettingsFeedback(), + SettingsAttribution(), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 109), + ], + ), + ); + } else { + // Mobile layout + return Scaffold( + body: _AnimatedHomePage( + restorationId: 'animated_page', + isSplashPageAnimationFinished: + SplashPageAnimation.of(context)!.isFinished, + carouselCards: carouselCards, + ), + ); + } + } + + List spaceBetween(double paddingBetween, List children) { + return [ + for (int index = 0; index < children.length; index++) ...[ + Flexible( + child: children[index], + ), + if (index < children.length - 1) SizedBox(width: paddingBetween), + ], + ]; + } +} + +class _GalleryHeader extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Header( + color: Theme.of(context).colorScheme.primaryContainer, + text: GalleryLocalizations.of(context)!.homeHeaderGallery, + ); + } +} + +class _CategoriesHeader extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Header( + color: Theme.of(context).colorScheme.primary, + text: GalleryLocalizations.of(context)!.homeHeaderCategories, + ); + } +} + +class Header extends StatelessWidget { + const Header({super.key, required this.color, required this.text}); + + final Color color; + final String text; + + @override + Widget build(BuildContext context) { + return Align( + alignment: AlignmentDirectional.centerStart, + child: Padding( + padding: EdgeInsets.only( + top: isDisplayDesktop(context) ? 63 : 15, + bottom: isDisplayDesktop(context) ? 21 : 11, + ), + child: SelectableText( + text, + style: Theme.of(context).textTheme.headlineMedium!.apply( + color: color, + fontSizeDelta: + isDisplayDesktop(context) ? desktopDisplay1FontDelta : 0, + ), + ), + ), + ); + } +} + +class _AnimatedHomePage extends StatefulWidget { + const _AnimatedHomePage({ + required this.restorationId, + required this.carouselCards, + required this.isSplashPageAnimationFinished, + }); + + final String restorationId; + final List carouselCards; + final bool isSplashPageAnimationFinished; + + @override + _AnimatedHomePageState createState() => _AnimatedHomePageState(); +} + +class _AnimatedHomePageState extends State<_AnimatedHomePage> + with RestorationMixin, SingleTickerProviderStateMixin { + late AnimationController _animationController; + Timer? _launchTimer; + final RestorableBool _isMaterialListExpanded = RestorableBool(false); + final RestorableBool _isCupertinoListExpanded = RestorableBool(false); + final RestorableBool _isOtherListExpanded = RestorableBool(false); + + @override + String get restorationId => widget.restorationId; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_isMaterialListExpanded, 'material_list'); + registerForRestoration(_isCupertinoListExpanded, 'cupertino_list'); + registerForRestoration(_isOtherListExpanded, 'other_list'); + } + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 800), + ); + + if (widget.isSplashPageAnimationFinished) { + // To avoid the animation from running when changing the window size from + // desktop to mobile, we do not animate our widget if the + // splash page animation is finished on initState. + _animationController.value = 1.0; + } else { + // Start our animation halfway through the splash page animation. + _launchTimer = Timer( + halfSplashPageAnimationDuration, + () { + _animationController.forward(); + }, + ); + } + } + + @override + void dispose() { + _animationController.dispose(); + _launchTimer?.cancel(); + _launchTimer = null; + _isMaterialListExpanded.dispose(); + _isCupertinoListExpanded.dispose(); + _isOtherListExpanded.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + final bool isTestMode = GalleryOptions.of(context).isTestMode; + return Stack( + children: [ + ListView( + // Makes integration tests possible. + key: const ValueKey('HomeListView'), + primary: true, + restorationId: 'home_list_view', + children: [ + const SizedBox(height: 8), + Container( + margin: + const EdgeInsets.symmetric(horizontal: _horizontalPadding), + child: _GalleryHeader(), + ), + _MobileCarousel( + animationController: _animationController, + restorationId: 'home_carousel', + children: widget.carouselCards, + ), + Container( + margin: + const EdgeInsets.symmetric(horizontal: _horizontalPadding), + child: _CategoriesHeader(), + ), + _AnimatedCategoryItem( + startDelayFraction: 0.00, + controller: _animationController, + child: CategoryListItem( + key: const PageStorageKey( + GalleryDemoCategory.material, + ), + restorationId: 'home_material_category_list', + category: GalleryDemoCategory.material, + imageString: 'assets/icons/material/material.png', + demos: Demos.materialDemos(localizations), + initiallyExpanded: + _isMaterialListExpanded.value || isTestMode, + onTap: (bool shouldOpenList) { + _isMaterialListExpanded.value = shouldOpenList; + }), + ), + _AnimatedCategoryItem( + startDelayFraction: 0.05, + controller: _animationController, + child: CategoryListItem( + key: const PageStorageKey( + GalleryDemoCategory.cupertino, + ), + restorationId: 'home_cupertino_category_list', + category: GalleryDemoCategory.cupertino, + imageString: 'assets/icons/cupertino/cupertino.png', + demos: Demos.cupertinoDemos(localizations), + initiallyExpanded: + _isCupertinoListExpanded.value || isTestMode, + onTap: (bool shouldOpenList) { + _isCupertinoListExpanded.value = shouldOpenList; + }), + ), + _AnimatedCategoryItem( + startDelayFraction: 0.10, + controller: _animationController, + child: CategoryListItem( + key: const PageStorageKey( + GalleryDemoCategory.other, + ), + restorationId: 'home_other_category_list', + category: GalleryDemoCategory.other, + imageString: 'assets/icons/reference/reference.png', + demos: Demos.otherDemos(localizations), + initiallyExpanded: _isOtherListExpanded.value || isTestMode, + onTap: (bool shouldOpenList) { + _isOtherListExpanded.value = shouldOpenList; + }), + ), + ], + ), + Align( + alignment: Alignment.topCenter, + child: GestureDetector( + onVerticalDragEnd: (DragEndDetails details) { + if (details.velocity.pixelsPerSecond.dy > 200) { + ToggleSplashNotification().dispatch(context); + } + }, + child: SafeArea( + child: Container( + height: 40, + // If we don't set the color, gestures are not detected. + color: Colors.transparent, + ), + ), + ), + ), + ], + ); + } +} + +class _DesktopHomeItem extends StatelessWidget { + const _DesktopHomeItem({required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Align( + child: Container( + constraints: const BoxConstraints(maxWidth: maxHomeItemWidth), + padding: const EdgeInsets.symmetric( + horizontal: _horizontalDesktopPadding, + ), + child: child, + ), + ); + } +} + +class _DesktopCategoryItem extends StatelessWidget { + const _DesktopCategoryItem({ + required this.category, + required this.asset, + required this.demos, + }); + + final GalleryDemoCategory category; + final ImageProvider asset; + final List demos; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + return Material( + borderRadius: BorderRadius.circular(10), + clipBehavior: Clip.antiAlias, + color: colorScheme.surface, + child: Semantics( + container: true, + child: FocusTraversalGroup( + policy: WidgetOrderTraversalPolicy(), + child: Column( + children: [ + _DesktopCategoryHeader( + category: category, + asset: asset, + ), + Divider( + height: 2, + thickness: 2, + color: colorScheme.background, + ), + Flexible( + child: ListView.builder( + // Makes integration tests possible. + key: ValueKey('${category.name}DemoList'), + primary: false, + itemBuilder: (BuildContext context, int index) => + CategoryDemoItem(demo: demos[index]), + itemCount: demos.length, + ), + ), + ], + ), + ), + ), + ); + } +} + +class _DesktopCategoryHeader extends StatelessWidget { + const _DesktopCategoryHeader({ + required this.category, + required this.asset, + }); + + final GalleryDemoCategory category; + final ImageProvider asset; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + return Material( + // Makes integration tests possible. + key: ValueKey('${category.name}CategoryHeader'), + color: colorScheme.onBackground, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: FadeInImage( + image: asset, + placeholder: MemoryImage(kTransparentImage), + fadeInDuration: entranceAnimationDuration, + width: 64, + height: 64, + excludeFromSemantics: true, + ), + ), + Flexible( + child: Padding( + padding: const EdgeInsetsDirectional.only(start: 8), + child: Semantics( + header: true, + child: SelectableText( + category.displayTitle(GalleryLocalizations.of(context)!)!, + style: Theme.of(context).textTheme.headlineSmall!.apply( + color: colorScheme.onSurface, + ), + ), + ), + ), + ), + ], + ), + ); + } +} + +/// Animates the category item to stagger in. The [_AnimatedCategoryItem.startDelayFraction] +/// gives a delay in the unit of a fraction of the whole animation duration, +/// which is defined in [_AnimatedHomePageState]. +class _AnimatedCategoryItem extends StatelessWidget { + _AnimatedCategoryItem({ + required double startDelayFraction, + required this.controller, + required this.child, + }) : topPaddingAnimation = Tween( + begin: 60.0, + end: 0.0, + ).animate( + CurvedAnimation( + parent: controller, + curve: Interval( + 0.000 + startDelayFraction, + 0.400 + startDelayFraction, + curve: Curves.ease, + ), + ), + ); + + final Widget child; + final AnimationController controller; + final Animation topPaddingAnimation; + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: controller, + builder: (BuildContext context, Widget? child) { + return Padding( + padding: EdgeInsets.only(top: topPaddingAnimation.value), + child: child, + ); + }, + child: child, + ); + } +} + +/// Animates the carousel to come in from the right. +class _AnimatedCarousel extends StatelessWidget { + _AnimatedCarousel({ + required this.child, + required this.controller, + }) : startPositionAnimation = Tween( + begin: 1.0, + end: 0.0, + ).animate( + CurvedAnimation( + parent: controller, + curve: const Interval( + 0.200, + 0.800, + curve: Curves.ease, + ), + ), + ); + + final Widget child; + final AnimationController controller; + final Animation startPositionAnimation; + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { + return Stack( + children: [ + SizedBox(height: _carouselHeight(.4, context)), + AnimatedBuilder( + animation: controller, + builder: (BuildContext context, Widget? child) { + return PositionedDirectional( + start: constraints.maxWidth * startPositionAnimation.value, + child: child!, + ); + }, + child: SizedBox( + height: _carouselHeight(.4, context), + width: constraints.maxWidth, + child: child, + ), + ), + ], + ); + }); + } +} + +/// Animates a carousel card to come in from the right. +class _AnimatedCarouselCard extends StatelessWidget { + _AnimatedCarouselCard({ + required this.child, + required this.controller, + }) : startPaddingAnimation = Tween( + begin: _horizontalPadding, + end: 0.0, + ).animate( + CurvedAnimation( + parent: controller, + curve: const Interval( + 0.900, + 1.000, + curve: Curves.ease, + ), + ), + ); + + final Widget child; + final AnimationController controller; + final Animation startPaddingAnimation; + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: controller, + builder: (BuildContext context, Widget? child) { + return Padding( + padding: EdgeInsetsDirectional.only( + start: startPaddingAnimation.value, + ), + child: child, + ); + }, + child: child, + ); + } +} + +class _MobileCarousel extends StatefulWidget { + const _MobileCarousel({ + required this.animationController, + this.restorationId, + required this.children, + }); + + final AnimationController animationController; + final String? restorationId; + final List children; + + @override + _MobileCarouselState createState() => _MobileCarouselState(); +} + +class _MobileCarouselState extends State<_MobileCarousel> + with RestorationMixin, SingleTickerProviderStateMixin { + late PageController _controller; + + final RestorableInt _currentPage = RestorableInt(0); + + @override + String? get restorationId => widget.restorationId; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_currentPage, 'carousel_page'); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // The viewPortFraction is calculated as the width of the device minus the + // padding. + final double width = MediaQuery.of(context).size.width; + const double padding = _carouselItemMobileMargin * 2; + _controller = PageController( + initialPage: _currentPage.value, + viewportFraction: (_carouselItemWidth + padding) / width, + ); + } + + @override + void dispose() { + _controller.dispose(); + _currentPage.dispose(); + super.dispose(); + } + + Widget builder(int index) { + final AnimatedBuilder carouselCard = AnimatedBuilder( + animation: _controller, + builder: (BuildContext context, Widget? child) { + double value; + if (_controller.position.haveDimensions) { + value = _controller.page! - index; + } else { + // If haveDimensions is false, use _currentPage to calculate value. + value = (_currentPage.value - index).toDouble(); + } + // .3 is an approximation of the curve used in the design. + value = (1 - (value.abs() * .3)).clamp(0, 1).toDouble(); + value = Curves.easeOut.transform(value); + + return Transform.scale( + scale: value, + child: child, + ); + }, + child: widget.children[index], + ); + + // We only want the second card to be animated. + if (index == 1) { + return _AnimatedCarouselCard( + controller: widget.animationController, + child: carouselCard, + ); + } else { + return carouselCard; + } + } + + @override + Widget build(BuildContext context) { + return _AnimatedCarousel( + controller: widget.animationController, + child: PageView.builder( + // Makes integration tests possible. + key: const ValueKey('studyDemoList'), + onPageChanged: (int value) { + setState(() { + _currentPage.value = value; + }); + }, + controller: _controller, + pageSnapping: false, + itemCount: widget.children.length, + itemBuilder: (BuildContext context, int index) => builder(index), + allowImplicitScrolling: true, + ), + ); + } +} + +/// This creates a horizontally scrolling [ListView] of items. +/// +/// This class uses a [ListView] with a custom [ScrollPhysics] to enable +/// snapping behavior. A [PageView] was considered but does not allow for +/// multiple pages visible without centering the first page. +class _DesktopCarousel extends StatefulWidget { + const _DesktopCarousel({required this.height, required this.children}); + + final double height; + final List children; + + @override + _DesktopCarouselState createState() => _DesktopCarouselState(); +} + +class _DesktopCarouselState extends State<_DesktopCarousel> { + late ScrollController _controller; + + @override + void initState() { + super.initState(); + _controller = ScrollController(); + _controller.addListener(() { + setState(() {}); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + bool showPreviousButton = false; + bool showNextButton = true; + // Only check this after the _controller has been attached to the ListView. + if (_controller.hasClients) { + showPreviousButton = _controller.offset > 0; + showNextButton = + _controller.offset < _controller.position.maxScrollExtent; + } + + final bool isDesktop = isDisplayDesktop(context); + + return Align( + child: Container( + height: widget.height, + constraints: const BoxConstraints(maxWidth: maxHomeItemWidth), + child: Stack( + children: [ + ListView.builder( + padding: EdgeInsets.symmetric( + horizontal: isDesktop + ? _horizontalDesktopPadding - _carouselItemDesktopMargin + : _horizontalPadding - _carouselItemMobileMargin, + ), + scrollDirection: Axis.horizontal, + primary: false, + physics: const _SnappingScrollPhysics(), + controller: _controller, + itemExtent: _carouselItemWidth, + itemCount: widget.children.length, + itemBuilder: (BuildContext context, int index) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: widget.children[index], + ), + ), + if (showPreviousButton) + _DesktopPageButton( + onTap: () { + _controller.animateTo( + _controller.offset - _carouselItemWidth, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + ); + }, + ), + if (showNextButton) + _DesktopPageButton( + isEnd: true, + onTap: () { + _controller.animateTo( + _controller.offset + _carouselItemWidth, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + ); + }, + ), + ], + ), + ), + ); + } +} + +/// Scrolling physics that snaps to the new item in the [_DesktopCarousel]. +class _SnappingScrollPhysics extends ScrollPhysics { + const _SnappingScrollPhysics({super.parent}); + + @override + _SnappingScrollPhysics applyTo(ScrollPhysics? ancestor) { + return _SnappingScrollPhysics(parent: buildParent(ancestor)); + } + + double _getTargetPixels( + ScrollMetrics position, + Tolerance tolerance, + double velocity, + ) { + final double itemWidth = position.viewportDimension / 4; + double item = position.pixels / itemWidth; + if (velocity < -tolerance.velocity) { + item -= 0.5; + } else if (velocity > tolerance.velocity) { + item += 0.5; + } + return math.min( + item.roundToDouble() * itemWidth, + position.maxScrollExtent, + ); + } + + @override + Simulation? createBallisticSimulation( + ScrollMetrics position, + double velocity, + ) { + if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) || + (velocity >= 0.0 && position.pixels >= position.maxScrollExtent)) { + return super.createBallisticSimulation(position, velocity); + } + final Tolerance tolerance = toleranceFor(position); + final double target = _getTargetPixels(position, tolerance, velocity); + if (target != position.pixels) { + return ScrollSpringSimulation( + spring, + position.pixels, + target, + velocity, + tolerance: tolerance, + ); + } + return null; + } + + @override + bool get allowImplicitScrolling => true; +} + +class _DesktopPageButton extends StatelessWidget { + const _DesktopPageButton({ + this.isEnd = false, + this.onTap, + }); + + final bool isEnd; + final GestureTapCallback? onTap; + + @override + Widget build(BuildContext context) { + const double buttonSize = 58.0; + const double padding = _horizontalDesktopPadding - buttonSize / 2; + return ExcludeSemantics( + child: Align( + alignment: isEnd + ? AlignmentDirectional.centerEnd + : AlignmentDirectional.centerStart, + child: Container( + width: buttonSize, + height: buttonSize, + margin: EdgeInsetsDirectional.only( + start: isEnd ? 0 : padding, + end: isEnd ? padding : 0, + ), + child: Tooltip( + message: isEnd + ? MaterialLocalizations.of(context).nextPageTooltip + : MaterialLocalizations.of(context).previousPageTooltip, + child: Material( + color: Colors.black.withOpacity(0.5), + shape: const CircleBorder(), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: onTap, + child: Icon( + isEnd ? Icons.arrow_forward_ios : Icons.arrow_back_ios, + color: Colors.white, + ), + ), + ), + ), + ), + ), + ); + } +} + +class _CarouselCard extends StatelessWidget { + const _CarouselCard({ + required this.demo, + this.asset, + this.assetDark, + this.assetColor, + this.assetDarkColor, + this.textColor, + required this.studyRoute, + }); + + final GalleryDemo? demo; + final ImageProvider? asset; + final ImageProvider? assetDark; + final Color? assetColor; + final Color? assetDarkColor; + final Color? textColor; + final String studyRoute; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + final bool isDark = Theme.of(context).colorScheme.brightness == Brightness.dark; + final ImageProvider? asset = isDark ? assetDark : this.asset; + final Color? assetColor = isDark ? assetDarkColor : this.assetColor; + final Color? textColor = isDark ? Colors.white.withOpacity(0.87) : this.textColor; + final bool isDesktop = isDisplayDesktop(context); + + return Container( + padding: EdgeInsets.symmetric( + horizontal: isDesktop + ? _carouselItemDesktopMargin + : _carouselItemMobileMargin), + margin: const EdgeInsets.symmetric(vertical: 16.0), + height: _carouselHeight(0.7, context), + width: _carouselItemWidth, + child: Material( + // Makes integration tests possible. + key: ValueKey(demo!.describe), + color: assetColor, + elevation: 4, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + clipBehavior: Clip.antiAlias, + child: Stack( + fit: StackFit.expand, + children: [ + if (asset != null) + FadeInImage( + image: asset, + placeholder: MemoryImage(kTransparentImage), + fit: BoxFit.cover, + height: _carouselHeightMin, + fadeInDuration: entranceAnimationDuration, + ), + Padding( + padding: const EdgeInsetsDirectional.fromSTEB(16, 0, 16, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + demo!.title, + style: textTheme.bodySmall!.apply(color: textColor), + maxLines: 3, + overflow: TextOverflow.visible, + ), + Text( + demo!.subtitle, + style: textTheme.labelSmall!.apply(color: textColor), + maxLines: 5, + overflow: TextOverflow.visible, + ), + ], + ), + ), + Positioned.fill( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + Navigator.of(context) + .popUntil((Route route) => route.settings.name == '/'); + Navigator.of(context).restorablePushNamed(studyRoute); + }, + ), + ), + ), + ], + ), + ), + ); + } +} + +double _carouselHeight(double scaleFactor, BuildContext context) => math.max( + _carouselHeightMin * + GalleryOptions.of(context).textScaleFactor(context) * + scaleFactor, + _carouselHeightMin); + +/// Wrap the studies with this to display a back button and allow the user to +/// exit them at any time. +class StudyWrapper extends StatefulWidget { + const StudyWrapper({ + super.key, + required this.study, + this.alignment = AlignmentDirectional.bottomStart, + this.hasBottomNavBar = false, + }); + + final Widget study; + final bool hasBottomNavBar; + final AlignmentDirectional alignment; + + @override + State createState() => _StudyWrapperState(); +} + +class _StudyWrapperState extends State { + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + final TextTheme textTheme = Theme.of(context).textTheme; + return ApplyTextOptions( + child: Stack( + children: [ + Semantics( + sortKey: const OrdinalSortKey(1), + child: RestorationScope( + restorationId: 'study_wrapper', + child: widget.study, + ), + ), + if (!isDisplayFoldable(context)) + SafeArea( + child: Align( + alignment: widget.alignment, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 16.0, + vertical: widget.hasBottomNavBar + ? kBottomNavigationBarHeight + 16.0 + : 16.0), + child: Semantics( + sortKey: const OrdinalSortKey(0), + label: GalleryLocalizations.of(context)!.backToGallery, + button: true, + enabled: true, + excludeSemantics: true, + child: FloatingActionButton.extended( + heroTag: _BackButtonHeroTag(), + key: const ValueKey('Back'), + onPressed: () { + Navigator.of(context) + .popUntil((Route route) => route.settings.name == '/'); + }, + icon: IconTheme( + data: IconThemeData(color: colorScheme.onPrimary), + child: const BackButtonIcon(), + ), + label: Text( + MaterialLocalizations.of(context).backButtonTooltip, + style: textTheme.labelLarge! + .apply(color: colorScheme.onPrimary), + ), + ), + ), + ), + ), + ), + ], + ), + ); + } +} + +class _BackButtonHeroTag {} diff --git a/dev/integration_tests/new_gallery/lib/pages/settings.dart b/dev/integration_tests/new_gallery/lib/pages/settings.dart new file mode 100644 index 0000000000..182fdb5602 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/pages/settings.dart @@ -0,0 +1,472 @@ +// 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 'dart:collection'; + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localized_locales/flutter_localized_locales.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../constants.dart'; +import '../data/gallery_options.dart'; +import '../gallery_localizations.dart'; +import '../layout/adaptive.dart'; +import 'about.dart' as about; +import 'home.dart'; +import 'settings_list_item.dart'; + +enum _ExpandableSetting { + textScale, + textDirection, + locale, + platform, + theme, +} + +class SettingsPage extends StatefulWidget { + const SettingsPage({ + super.key, + required this.animationController, + }); + + final AnimationController animationController; + + @override + State createState() => _SettingsPageState(); +} + +class _SettingsPageState extends State { + _ExpandableSetting? _expandedSettingId; + late Animation _staggerSettingsItemsAnimation; + + void onTapSetting(_ExpandableSetting settingId) { + setState(() { + if (_expandedSettingId == settingId) { + _expandedSettingId = null; + } else { + _expandedSettingId = settingId; + } + }); + } + + void _closeSettingId(AnimationStatus status) { + if (status == AnimationStatus.dismissed) { + setState(() { + _expandedSettingId = null; + }); + } + } + + @override + void initState() { + super.initState(); + + // When closing settings, also shrink expanded setting. + widget.animationController.addStatusListener(_closeSettingId); + + _staggerSettingsItemsAnimation = CurvedAnimation( + parent: widget.animationController, + curve: const Interval( + 0.4, + 1.0, + curve: Curves.ease, + ), + ); + } + + @override + void dispose() { + super.dispose(); + widget.animationController.removeStatusListener(_closeSettingId); + } + + /// Given a [Locale], returns a [DisplayOption] with its native name for a + /// title and its name in the currently selected locale for a subtitle. If the + /// native name can't be determined, it is omitted. If the locale can't be + /// determined, the locale code is used. + DisplayOption _getLocaleDisplayOption(BuildContext context, Locale? locale) { + final String localeCode = locale.toString(); + final String? localeName = LocaleNames.of(context)!.nameOf(localeCode); + if (localeName != null) { + final String? localeNativeName = + LocaleNamesLocalizationsDelegate.nativeLocaleNames[localeCode]; + return localeNativeName != null + ? DisplayOption(localeNativeName, subtitle: localeName) + : DisplayOption(localeName); + } else { + // gsw, fil, and es_419 aren't in flutter_localized_countries' dataset + // so we handle them separately + switch (localeCode) { + case 'gsw': + return DisplayOption('Schwiizertüütsch', subtitle: 'Swiss German'); + case 'fil': + return DisplayOption('Filipino', subtitle: 'Filipino'); + case 'es_419': + return DisplayOption( + 'español (Latinoamérica)', + subtitle: 'Spanish (Latin America)', + ); + } + } + + return DisplayOption(localeCode); + } + + /// Create a sorted — by native name – map of supported locales to their + /// intended display string, with a system option as the first element. + LinkedHashMap _getLocaleOptions() { + final LinkedHashMap localeOptions = LinkedHashMap.of({ + systemLocaleOption: DisplayOption( + GalleryLocalizations.of(context)!.settingsSystemDefault + + (deviceLocale != null + ? ' - ${_getLocaleDisplayOption(context, deviceLocale).title}' + : ''), + ), + }); + final List supportedLocales = + List.from(GalleryLocalizations.supportedLocales); + supportedLocales.removeWhere((Locale locale) => locale == deviceLocale); + + final List> displayLocales = Map.fromIterable( + supportedLocales, + value: (dynamic locale) => + _getLocaleDisplayOption(context, locale as Locale?), + ).entries.toList() + ..sort((MapEntry l1, MapEntry l2) => compareAsciiUpperCase(l1.value.title, l2.value.title)); + + localeOptions.addAll(LinkedHashMap.fromEntries(displayLocales)); + return localeOptions; + } + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + final GalleryOptions options = GalleryOptions.of(context); + final bool isDesktop = isDisplayDesktop(context); + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + final List settingsListItems = [ + SettingsListItem( + title: localizations.settingsTextScaling, + selectedOption: options.textScaleFactor( + context, + useSentinel: true, + ), + optionsMap: LinkedHashMap.of({ + systemTextScaleFactorOption: DisplayOption( + localizations.settingsSystemDefault, + ), + 0.8: DisplayOption( + localizations.settingsTextScalingSmall, + ), + 1.0: DisplayOption( + localizations.settingsTextScalingNormal, + ), + 2.0: DisplayOption( + localizations.settingsTextScalingLarge, + ), + 3.0: DisplayOption( + localizations.settingsTextScalingHuge, + ), + }), + onOptionChanged: (double? newTextScale) => GalleryOptions.update( + context, + options.copyWith(textScaleFactor: newTextScale), + ), + onTapSetting: () => onTapSetting(_ExpandableSetting.textScale), + isExpanded: _expandedSettingId == _ExpandableSetting.textScale, + ), + SettingsListItem( + title: localizations.settingsTextDirection, + selectedOption: options.customTextDirection, + optionsMap: LinkedHashMap.of({ + CustomTextDirection.localeBased: DisplayOption( + localizations.settingsTextDirectionLocaleBased, + ), + CustomTextDirection.ltr: DisplayOption( + localizations.settingsTextDirectionLTR, + ), + CustomTextDirection.rtl: DisplayOption( + localizations.settingsTextDirectionRTL, + ), + }), + onOptionChanged: (CustomTextDirection? newTextDirection) => GalleryOptions.update( + context, + options.copyWith(customTextDirection: newTextDirection), + ), + onTapSetting: () => onTapSetting(_ExpandableSetting.textDirection), + isExpanded: _expandedSettingId == _ExpandableSetting.textDirection, + ), + SettingsListItem( + title: localizations.settingsLocale, + selectedOption: options.locale == deviceLocale + ? systemLocaleOption + : options.locale, + optionsMap: _getLocaleOptions(), + onOptionChanged: (Locale? newLocale) { + if (newLocale == systemLocaleOption) { + newLocale = deviceLocale; + } + GalleryOptions.update( + context, + options.copyWith(locale: newLocale), + ); + }, + onTapSetting: () => onTapSetting(_ExpandableSetting.locale), + isExpanded: _expandedSettingId == _ExpandableSetting.locale, + ), + SettingsListItem( + title: localizations.settingsPlatformMechanics, + selectedOption: options.platform, + optionsMap: LinkedHashMap.of({ + TargetPlatform.android: DisplayOption('Android'), + TargetPlatform.iOS: DisplayOption('iOS'), + TargetPlatform.macOS: DisplayOption('macOS'), + TargetPlatform.linux: DisplayOption('Linux'), + TargetPlatform.windows: DisplayOption('Windows'), + }), + onOptionChanged: (TargetPlatform? newPlatform) => GalleryOptions.update( + context, + options.copyWith(platform: newPlatform), + ), + onTapSetting: () => onTapSetting(_ExpandableSetting.platform), + isExpanded: _expandedSettingId == _ExpandableSetting.platform, + ), + SettingsListItem( + title: localizations.settingsTheme, + selectedOption: options.themeMode, + optionsMap: LinkedHashMap.of({ + ThemeMode.system: DisplayOption( + localizations.settingsSystemDefault, + ), + ThemeMode.dark: DisplayOption( + localizations.settingsDarkTheme, + ), + ThemeMode.light: DisplayOption( + localizations.settingsLightTheme, + ), + }), + onOptionChanged: (ThemeMode? newThemeMode) => GalleryOptions.update( + context, + options.copyWith(themeMode: newThemeMode), + ), + onTapSetting: () => onTapSetting(_ExpandableSetting.theme), + isExpanded: _expandedSettingId == _ExpandableSetting.theme, + ), + ToggleSetting( + text: GalleryLocalizations.of(context)!.settingsSlowMotion, + value: options.timeDilation != 1.0, + onChanged: (bool isOn) => GalleryOptions.update( + context, + options.copyWith(timeDilation: isOn ? 5.0 : 1.0), + ), + ), + ]; + + return Material( + color: colorScheme.secondaryContainer, + child: Padding( + padding: isDesktop + ? EdgeInsets.zero + : const EdgeInsets.only( + bottom: galleryHeaderHeight, + ), + // Remove ListView top padding as it is already accounted for. + child: MediaQuery.removePadding( + removeTop: isDesktop, + context: context, + child: ListView( + children: [ + if (isDesktop) + const SizedBox(height: firstHeaderDesktopTopPadding), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: ExcludeSemantics( + child: Header( + color: Theme.of(context).colorScheme.onSurface, + text: localizations.settingsTitle, + ), + ), + ), + if (isDesktop) + ...settingsListItems + else ...[ + _AnimateSettingsListItems( + animation: _staggerSettingsItemsAnimation, + children: settingsListItems, + ), + const SizedBox(height: 16), + Divider(thickness: 2, height: 0, color: colorScheme.outline), + const SizedBox(height: 12), + const SettingsAbout(), + const SettingsFeedback(), + const SizedBox(height: 12), + Divider(thickness: 2, height: 0, color: colorScheme.outline), + const SettingsAttribution(), + ], + ], + ), + ), + ), + ); + } +} + +class SettingsAbout extends StatelessWidget { + const SettingsAbout({super.key}); + + @override + Widget build(BuildContext context) { + return _SettingsLink( + title: GalleryLocalizations.of(context)!.settingsAbout, + icon: Icons.info_outline, + onTap: () { + about.showAboutDialog(context: context); + }, + ); + } +} + +class SettingsFeedback extends StatelessWidget { + const SettingsFeedback({super.key}); + + @override + Widget build(BuildContext context) { + return _SettingsLink( + title: GalleryLocalizations.of(context)!.settingsFeedback, + icon: Icons.feedback, + onTap: () async { + final Uri url = + Uri.parse('https://github.com/flutter/gallery/issues/new/choose/'); + if (await canLaunchUrl(url)) { + await launchUrl(url); + } + }, + ); + } +} + +class SettingsAttribution extends StatelessWidget { + const SettingsAttribution({super.key}); + + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + final double verticalPadding = isDesktop ? 0.0 : 28.0; + return MergeSemantics( + child: Padding( + padding: EdgeInsetsDirectional.only( + start: isDesktop ? 24 : 32, + end: isDesktop ? 0 : 32, + top: verticalPadding, + bottom: verticalPadding, + ), + child: SelectableText( + GalleryLocalizations.of(context)!.settingsAttribution, + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontSize: 12, + color: Theme.of(context).colorScheme.onSecondary, + ), + textAlign: isDesktop ? TextAlign.end : TextAlign.start, + ), + ), + ); + } +} + +class _SettingsLink extends StatelessWidget { + + const _SettingsLink({ + required this.title, + this.icon, + this.onTap, + }); + final String title; + final IconData? icon; + final GestureTapCallback? onTap; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + final ColorScheme colorScheme = Theme.of(context).colorScheme; + final bool isDesktop = isDisplayDesktop(context); + + return InkWell( + onTap: onTap, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: isDesktop ? 24 : 32, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + color: colorScheme.onSecondary.withOpacity(0.5), + size: 24, + ), + Flexible( + child: Padding( + padding: const EdgeInsetsDirectional.only( + start: 16, + top: 12, + bottom: 12, + ), + child: Text( + title, + style: textTheme.titleSmall!.apply( + color: colorScheme.onSecondary, + ), + textAlign: isDesktop ? TextAlign.end : TextAlign.start, + ), + ), + ), + ], + ), + ), + ); + } +} + +/// Animate the settings list items to stagger in from above. +class _AnimateSettingsListItems extends StatelessWidget { + const _AnimateSettingsListItems({ + required this.animation, + required this.children, + }); + + final Animation animation; + final List children; + + @override + Widget build(BuildContext context) { + const double dividingPadding = 4.0; + final Tween dividerTween = Tween( + begin: 0, + end: dividingPadding, + ); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Column( + children: [ + for (final Widget child in children) + AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget? child) { + return Padding( + padding: EdgeInsets.only( + top: dividerTween.animate(animation).value, + ), + child: child, + ); + }, + child: child, + ), + ], + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/pages/settings_icon/icon.dart b/dev/integration_tests/new_gallery/lib/pages/settings_icon/icon.dart new file mode 100644 index 0000000000..889f2b55dc --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/pages/settings_icon/icon.dart @@ -0,0 +1,191 @@ +// 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 'dart:math'; +import 'package:flutter/material.dart'; +import 'metrics.dart'; + +class SettingsIcon extends StatelessWidget { + const SettingsIcon(this.time, {super.key}); + + final double time; + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: _SettingsIconPainter(time: time, context: context), + ); + } +} + +class _SettingsIconPainter extends CustomPainter { + _SettingsIconPainter({required this.time, required this.context}); + + final double time; + final BuildContext context; + + late Offset _center; + late double _scaling; + late Canvas _canvas; + + /// Computes [_center] and [_scaling], parameters used to convert offsets + /// and lengths in relative units into logical pixels. + /// + /// The icon is aligned to the bottom-start corner. + void _computeCenterAndScaling(Size size) { + _scaling = min(size.width / unitWidth, size.height / unitHeight); + _center = Directionality.of(context) == TextDirection.ltr + ? Offset( + unitWidth * _scaling / 2, size.height - unitHeight * _scaling / 2) + : Offset(size.width - unitWidth * _scaling / 2, + size.height - unitHeight * _scaling / 2); + } + + /// Transforms an offset in relative units into an offset in logical pixels. + Offset _transform(Offset offset) { + return _center + offset * _scaling; + } + + /// Transforms a length in relative units into a dimension in logical pixels. + double _size(double length) { + return length * _scaling; + } + + /// A rectangle with a fixed location, used to locate gradients. + Rect get _fixedRect { + final Offset topLeft = Offset(-_size(stickLength / 2), -_size(stickWidth / 2)); + final Offset bottomRight = Offset(_size(stickLength / 2), _size(stickWidth / 2)); + return Rect.fromPoints(topLeft, bottomRight); + } + + /// Black or white paint, depending on brightness. + Paint get _monoPaint { + final Color monoColor = + Theme.of(context).colorScheme.brightness == Brightness.light + ? Colors.black + : Colors.white; + return Paint()..color = monoColor; + } + + /// Pink paint with horizontal gradient. + Paint get _pinkPaint { + const LinearGradient shader = LinearGradient(colors: [pinkLeft, pinkRight]); + final Rect shaderRect = _fixedRect.translate( + _size(-(stickLength - colorLength(time)) / 2), + 0, + ); + + return Paint()..shader = shader.createShader(shaderRect); + } + + /// Teal paint with horizontal gradient. + Paint get _tealPaint { + const LinearGradient shader = LinearGradient(colors: [tealLeft, tealRight]); + final Rect shaderRect = _fixedRect.translate( + _size((stickLength - colorLength(time)) / 2), + 0, + ); + + return Paint()..shader = shader.createShader(shaderRect); + } + + /// Paints a stadium-shaped stick. + void _paintStick({ + required Offset center, + required double length, + required double width, + double angle = 0, + required Paint paint, + }) { + // Convert to pixels. + center = _transform(center); + length = _size(length); + width = _size(width); + + // Paint. + width = min(width, length); + final double stretch = length / 2; + final double radius = width / 2; + + _canvas.save(); + + _canvas.translate(center.dx, center.dy); + _canvas.rotate(angle); + + final Rect leftOval = Rect.fromCircle( + center: Offset(-stretch + radius, 0), + radius: radius, + ); + + final Rect rightOval = Rect.fromCircle( + center: Offset(stretch - radius, 0), + radius: radius, + ); + + _canvas.drawPath( + Path() + ..arcTo(leftOval, pi / 2, pi, false) + ..arcTo(rightOval, -pi / 2, pi, false), + paint, + ); + + _canvas.restore(); + } + + @override + void paint(Canvas canvas, Size size) { + _computeCenterAndScaling(size); + _canvas = canvas; + + if (isTransitionPhase(time)) { + _paintStick( + center: upperColorOffset(time), + length: colorLength(time), + width: stickWidth, + paint: _pinkPaint, + ); + + _paintStick( + center: lowerColorOffset(time), + length: colorLength(time), + width: stickWidth, + paint: _tealPaint, + ); + + _paintStick( + center: upperMonoOffset(time), + length: monoLength(time), + width: knobDiameter, + paint: _monoPaint, + ); + + _paintStick( + center: lowerMonoOffset(time), + length: monoLength(time), + width: knobDiameter, + paint: _monoPaint, + ); + } else { + _paintStick( + center: upperKnobCenter, + length: stickLength, + width: knobDiameter, + angle: -knobRotation(time), + paint: _monoPaint, + ); + + _paintStick( + center: knobCenter(time), + length: stickLength, + width: knobDiameter, + angle: knobRotation(time), + paint: _monoPaint, + ); + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => + oldDelegate is! _SettingsIconPainter || oldDelegate.time != time; +} diff --git a/dev/integration_tests/new_gallery/lib/pages/settings_icon/metrics.dart b/dev/integration_tests/new_gallery/lib/pages/settings_icon/metrics.dart new file mode 100644 index 0000000000..ffd59a8697 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/pages/settings_icon/metrics.dart @@ -0,0 +1,113 @@ +// 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 'dart:math'; +import 'package:flutter/material.dart'; + +// Color gradients. +const Color pinkLeft = Color(0xFFFF5983); +const Color pinkRight = Color(0xFFFF8383); + +const Color tealLeft = Color(0xFF1CDDC8); +const Color tealRight = Color(0xFF00A5B3); + +// Dimensions. +const int unitHeight = 1; +const int unitWidth = 1; + +const double stickLength = 5 / 9; +const double stickWidth = 5 / 36; +const double stickRadius = stickWidth / 2; +const double knobDiameter = 5 / 54; +const double knobRadius = knobDiameter / 2; +const double stickGap = 5 / 54; + +// Locations. +const double knobDistanceFromCenter = stickGap / 2 + stickWidth / 2; +const Offset lowerKnobCenter = Offset(0, knobDistanceFromCenter); +const Offset upperKnobCenter = Offset(0, -knobDistanceFromCenter); + +const double knobDeviation = stickLength / 2 - stickRadius; + +// Key moments in animation. +const double _colorKnobContractionBegins = 1 / 23; +const double _monoKnobExpansionEnds = 11 / 23; +const double _colorKnobContractionEnds = 14 / 23; + +// Stages. +bool isTransitionPhase(double time) => time < _colorKnobContractionEnds; + +// Curve easing. +const Cubic _curve = Curves.easeInOutCubic; + +double _progress( + double time, { + required double begin, + required double end, +}) => + _curve.transform(((time - begin) / (end - begin)).clamp(0, 1).toDouble()); + +double _monoKnobProgress(double time) => _progress( + time, + begin: 0, + end: _monoKnobExpansionEnds, + ); + +double _colorKnobProgress(double time) => _progress( + time, + begin: _colorKnobContractionBegins, + end: _colorKnobContractionEnds, + ); + +double _rotationProgress(double time) => _progress( + time, + begin: _colorKnobContractionEnds, + end: 1, + ); + +// Changing lengths: mono. +double monoLength(double time) => + _monoKnobProgress(time) * (stickLength - knobDiameter) + knobDiameter; + +double _monoLengthLeft(double time) => + min(monoLength(time) - knobRadius, stickRadius); + +double _monoLengthRight(double time) => + monoLength(time) - _monoLengthLeft(time); + +double _monoHorizontalOffset(double time) => + (_monoLengthRight(time) - _monoLengthLeft(time)) / 2 - knobDeviation; + +Offset upperMonoOffset(double time) => + upperKnobCenter + Offset(_monoHorizontalOffset(time), 0); + +Offset lowerMonoOffset(double time) => + lowerKnobCenter + Offset(-_monoHorizontalOffset(time), 0); + +// Changing lengths: color. +double colorLength(double time) => (1 - _colorKnobProgress(time)) * stickLength; + +Offset upperColorOffset(double time) => + upperKnobCenter + Offset(stickLength / 2 - colorLength(time) / 2, 0); + +Offset lowerColorOffset(double time) => + lowerKnobCenter + Offset(-stickLength / 2 + colorLength(time) / 2, 0); + +// Moving objects. +double knobRotation(double time) => _rotationProgress(time) * pi / 4; + +Offset knobCenter(double time) { + final double progress = _rotationProgress(time); + if (progress == 0) { + return lowerKnobCenter; + } else if (progress == 1) { + return upperKnobCenter; + } else { + // Calculates the current location. + final Offset center = Offset(knobDistanceFromCenter / tan(pi / 8), 0); + final double radius = (lowerKnobCenter - center).distance; + final double angle = pi + (progress - 1 / 2) * pi / 4; + return center + Offset.fromDirection(angle, radius); + } +} diff --git a/dev/integration_tests/new_gallery/lib/pages/settings_list_item.dart b/dev/integration_tests/new_gallery/lib/pages/settings_list_item.dart new file mode 100644 index 0000000000..6fcfebe761 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/pages/settings_list_item.dart @@ -0,0 +1,340 @@ +// 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 'dart:collection'; + +import 'package:flutter/material.dart'; + +// Common constants between SlowMotionSetting and SettingsListItem. +final BorderRadius settingItemBorderRadius = BorderRadius.circular(10); +const EdgeInsetsDirectional settingItemHeaderMargin = EdgeInsetsDirectional.fromSTEB(32, 0, 32, 8); + +class DisplayOption { + + DisplayOption(this.title, {this.subtitle}); + final String title; + final String? subtitle; +} + +class ToggleSetting extends StatelessWidget { + + const ToggleSetting({ + super.key, + required this.text, + required this.value, + required this.onChanged, + }); + final String text; + final bool value; + final void Function(bool) onChanged; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + final TextTheme textTheme = Theme.of(context).textTheme; + + return Semantics( + container: true, + child: Container( + margin: settingItemHeaderMargin, + child: Material( + shape: RoundedRectangleBorder(borderRadius: settingItemBorderRadius), + color: colorScheme.secondary, + clipBehavior: Clip.antiAlias, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SelectableText( + text, + style: textTheme.titleMedium!.apply( + color: colorScheme.onSurface, + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only(end: 8), + child: Switch( + activeColor: colorScheme.primary, + value: value, + onChanged: onChanged, + ), + ), + ], + ), + ), + ), + ); + } +} + +class SettingsListItem extends StatefulWidget { + const SettingsListItem({ + super.key, + required this.optionsMap, + required this.title, + required this.selectedOption, + required this.onOptionChanged, + required this.onTapSetting, + required this.isExpanded, + }); + + final LinkedHashMap optionsMap; + final String title; + final T selectedOption; + final ValueChanged onOptionChanged; + final void Function() onTapSetting; + final bool isExpanded; + + @override + State> createState() => _SettingsListItemState(); +} + +class _SettingsListItemState extends State> + with SingleTickerProviderStateMixin { + static final Animatable _easeInTween = + CurveTween(curve: Curves.easeIn); + static const Duration _expandDuration = Duration(milliseconds: 150); + late AnimationController _controller; + late Animation _childrenHeightFactor; + late Animation _headerChevronRotation; + late Animation _headerSubtitleHeight; + late Animation _headerMargin; + late Animation _headerPadding; + late Animation _childrenPadding; + late Animation _headerBorderRadius; + + // For ease of use. Correspond to the keys and values of `widget.optionsMap`. + late Iterable _options; + late Iterable _displayOptions; + + @override + void initState() { + super.initState(); + _controller = AnimationController(duration: _expandDuration, vsync: this); + _childrenHeightFactor = _controller.drive(_easeInTween); + _headerChevronRotation = + Tween(begin: 0, end: 0.5).animate(_controller); + _headerMargin = EdgeInsetsGeometryTween( + begin: settingItemHeaderMargin, + end: EdgeInsets.zero, + ).animate(_controller); + _headerPadding = EdgeInsetsGeometryTween( + begin: const EdgeInsetsDirectional.fromSTEB(16, 10, 0, 10), + end: const EdgeInsetsDirectional.fromSTEB(32, 18, 32, 20), + ).animate(_controller); + _headerSubtitleHeight = + _controller.drive(Tween(begin: 1.0, end: 0.0)); + _childrenPadding = EdgeInsetsGeometryTween( + begin: const EdgeInsets.symmetric(horizontal: 32), + end: EdgeInsets.zero, + ).animate(_controller); + _headerBorderRadius = BorderRadiusTween( + begin: settingItemBorderRadius, + end: BorderRadius.zero, + ).animate(_controller); + + if (widget.isExpanded) { + _controller.value = 1.0; + } + + _options = widget.optionsMap.keys; + _displayOptions = widget.optionsMap.values; + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _handleExpansion() { + if (widget.isExpanded) { + _controller.forward(); + } else { + _controller.reverse().then((void value) { + if (!mounted) { + return; + } + }); + } + } + + Widget _buildHeaderWithChildren(BuildContext context, Widget? child) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + _CategoryHeader( + margin: _headerMargin.value, + padding: _headerPadding.value, + borderRadius: _headerBorderRadius.value!, + subtitleHeight: _headerSubtitleHeight, + chevronRotation: _headerChevronRotation, + title: widget.title, + subtitle: widget.optionsMap[widget.selectedOption]?.title ?? '', + onTap: () => widget.onTapSetting(), + ), + Padding( + padding: _childrenPadding.value, + child: ClipRect( + child: Align( + heightFactor: _childrenHeightFactor.value, + child: child, + ), + ), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + _handleExpansion(); + final ThemeData theme = Theme.of(context); + + return AnimatedBuilder( + animation: _controller.view, + builder: _buildHeaderWithChildren, + child: Container( + constraints: const BoxConstraints(maxHeight: 384), + margin: const EdgeInsetsDirectional.only(start: 24, bottom: 40), + decoration: BoxDecoration( + border: BorderDirectional( + start: BorderSide( + width: 2, + color: theme.colorScheme.background, + ), + ), + ), + child: ListView.builder( + shrinkWrap: true, + itemCount: widget.isExpanded ? _options.length : 0, + itemBuilder: (BuildContext context, int index) { + final DisplayOption displayOption = _displayOptions.elementAt(index); + return RadioListTile( + value: _options.elementAt(index), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + displayOption.title, + style: theme.textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + if (displayOption.subtitle != null) + Text( + displayOption.subtitle!, + style: theme.textTheme.bodyLarge!.copyWith( + fontSize: 12, + color: Theme.of(context) + .colorScheme + .onPrimary + .withOpacity(0.8), + ), + ), + ], + ), + groupValue: widget.selectedOption, + onChanged: (T? newOption) => widget.onOptionChanged(newOption), + activeColor: Theme.of(context).colorScheme.primary, + dense: true, + ); + }, + ), + ), + ); + } +} + +class _CategoryHeader extends StatelessWidget { + const _CategoryHeader({ + this.margin, + required this.padding, + required this.borderRadius, + required this.subtitleHeight, + required this.chevronRotation, + required this.title, + required this.subtitle, + this.onTap, + }); + + final EdgeInsetsGeometry? margin; + final EdgeInsetsGeometry padding; + final BorderRadiusGeometry borderRadius; + final String title; + final String subtitle; + final Animation subtitleHeight; + final Animation chevronRotation; + final GestureTapCallback? onTap; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + final TextTheme textTheme = Theme.of(context).textTheme; + return Container( + margin: margin, + child: Material( + shape: RoundedRectangleBorder(borderRadius: borderRadius), + color: colorScheme.secondary, + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: onTap, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Padding( + padding: padding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + title, + style: textTheme.titleMedium!.apply( + color: colorScheme.onSurface, + ), + ), + SizeTransition( + sizeFactor: subtitleHeight, + child: Text( + subtitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.labelSmall!.apply( + color: colorScheme.primary, + ), + ), + ) + ], + ), + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only( + start: 8, + end: 24, + ), + child: RotationTransition( + turns: chevronRotation, + child: const Icon(Icons.arrow_drop_down), + ), + ) + ], + ), + ), + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/pages/splash.dart b/dev/integration_tests/new_gallery/lib/pages/splash.dart new file mode 100644 index 0000000000..9ee06e536b --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/pages/splash.dart @@ -0,0 +1,276 @@ +// 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 'dart:math'; + +import 'package:dual_screen/dual_screen.dart'; +import 'package:flutter/material.dart'; + +import '../constants.dart'; +import '../gallery_localizations.dart'; +import '../layout/adaptive.dart'; +import 'home.dart'; + +const double homePeekDesktop = 210.0; +const double homePeekMobile = 60.0; + +class SplashPageAnimation extends InheritedWidget { + const SplashPageAnimation({ + super.key, + required this.isFinished, + required super.child, + }); + + final bool isFinished; + + static SplashPageAnimation? of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); + } + + @override + bool updateShouldNotify(SplashPageAnimation oldWidget) => true; +} + +class SplashPage extends StatefulWidget { + const SplashPage({ + super.key, + required this.child, + }); + + final Widget child; + + @override + State createState() => _SplashPageState(); +} + +class _SplashPageState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late int _effect; + final Random _random = Random(); + + // A map of the effect index to its duration. This duration is used to + // determine how long to display the splash animation at launch. + // + // If a new effect is added, this map should be updated. + final Map _effectDurations = { + 1: 5, + 2: 4, + 3: 4, + 4: 5, + 5: 5, + 6: 4, + 7: 4, + 8: 4, + 9: 3, + 10: 6, + }; + + bool get _isSplashVisible { + return _controller.status == AnimationStatus.completed || + _controller.status == AnimationStatus.forward; + } + + @override + void initState() { + super.initState(); + + // If the number of included effects changes, this number should be changed. + _effect = _random.nextInt(_effectDurations.length) + 1; + + _controller = + AnimationController(duration: splashPageAnimationDuration, vsync: this) + ..addListener(() { + setState(() {}); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + Animation _getPanelAnimation( + BuildContext context, + BoxConstraints constraints, + ) { + final double height = constraints.biggest.height - + (isDisplayDesktop(context) ? homePeekDesktop : homePeekMobile); + return RelativeRectTween( + begin: RelativeRect.fill, + end: RelativeRect.fromLTRB(0, height, 0, 0), + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); + } + + @override + Widget build(BuildContext context) { + return NotificationListener( + onNotification: (_) { + _controller.forward(); + return true; + }, + child: SplashPageAnimation( + isFinished: _controller.status == AnimationStatus.dismissed, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + final Animation animation = _getPanelAnimation(context, constraints); + Widget frontLayer = widget.child; + if (_isSplashVisible) { + frontLayer = MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + _controller.reverse(); + }, + onVerticalDragEnd: (DragEndDetails details) { + if (details.velocity.pixelsPerSecond.dy < -200) { + _controller.reverse(); + } + }, + child: IgnorePointer(child: frontLayer), + ), + ); + } + + if (isDisplayDesktop(context)) { + frontLayer = Padding( + padding: const EdgeInsets.only(top: 136), + child: ClipRRect( + borderRadius: const BorderRadius.vertical( + top: Radius.circular(40), + ), + child: frontLayer, + ), + ); + } + + if (isDisplayFoldable(context)) { + return TwoPane( + startPane: frontLayer, + endPane: GestureDetector( + onTap: () { + if (_isSplashVisible) { + _controller.reverse(); + } else { + _controller.forward(); + } + }, + child: _SplashBackLayer( + isSplashCollapsed: !_isSplashVisible, effect: _effect), + ), + ); + } else { + return Stack( + children: [ + _SplashBackLayer( + isSplashCollapsed: !_isSplashVisible, + effect: _effect, + onTap: () { + _controller.forward(); + }, + ), + PositionedTransition( + rect: animation, + child: frontLayer, + ), + ], + ); + } + }, + ), + ), + ); + } +} + +class _SplashBackLayer extends StatelessWidget { + const _SplashBackLayer({ + required this.isSplashCollapsed, + required this.effect, + this.onTap, + }); + + final bool isSplashCollapsed; + final int effect; + final GestureTapCallback? onTap; + + @override + Widget build(BuildContext context) { + final String effectAsset = 'splash_effects/splash_effect_$effect.gif'; + final Image flutterLogo = Image.asset( + 'assets/logo/flutter_logo.png', + package: 'flutter_gallery_assets', + ); + + Widget? child; + if (isSplashCollapsed) { + if (isDisplayDesktop(context)) { + child = Padding( + padding: const EdgeInsets.only(top: 50), + child: Align( + alignment: Alignment.topCenter, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: onTap, + child: flutterLogo, + ), + ), + ), + ); + } + if (isDisplayFoldable(context)) { + child = ColoredBox( + color: Theme.of(context).colorScheme.background, + child: Stack( + children: [ + Center( + child: flutterLogo, + ), + Padding( + padding: const EdgeInsets.only(top: 100.0), + child: Center( + child: Text( + GalleryLocalizations.of(context)!.splashSelectDemo, + ), + ), + ) + ], + ), + ); + } + } else { + child = Stack( + children: [ + Center( + child: Image.asset( + effectAsset, + package: 'flutter_gallery_assets', + ), + ), + Center(child: flutterLogo), + ], + ); + } + + return ExcludeSemantics( + child: Material( + // This is the background color of the gifs. + color: const Color(0xFF030303), + child: Padding( + padding: EdgeInsets.only( + bottom: isDisplayDesktop(context) + ? homePeekDesktop + : isDisplayFoldable(context) + ? 0 + : homePeekMobile, + ), + child: child, + ), + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/routes.dart b/dev/integration_tests/new_gallery/lib/routes.dart new file mode 100644 index 0000000000..06f5eb3672 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/routes.dart @@ -0,0 +1,194 @@ +// 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:dual_screen/dual_screen.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'deferred_widget.dart'; +import 'main.dart'; +import 'pages/demo.dart'; +import 'pages/home.dart'; +import 'studies/crane/app.dart' deferred as crane; +import 'studies/crane/routes.dart' as crane_routes; +import 'studies/fortnightly/app.dart' deferred as fortnightly; +import 'studies/fortnightly/routes.dart' as fortnightly_routes; +import 'studies/rally/app.dart' deferred as rally; +import 'studies/rally/routes.dart' as rally_routes; +import 'studies/reply/app.dart' as reply; +import 'studies/reply/routes.dart' as reply_routes; +import 'studies/shrine/app.dart' deferred as shrine; +import 'studies/shrine/routes.dart' as shrine_routes; +import 'studies/starter/app.dart' as starter_app; +import 'studies/starter/routes.dart' as starter_app_routes; + +typedef PathWidgetBuilder = Widget Function(BuildContext, String?); + +class Path { + const Path(this.pattern, this.builder, {this.openInSecondScreen = false}); + + /// A RegEx string for route matching. + final String pattern; + + /// The builder for the associated pattern route. The first argument is the + /// [BuildContext] and the second argument a RegEx match if that is included + /// in the pattern. + /// + /// ```dart + /// Path( + /// 'r'^/demo/([\w-]+)$', + /// (context, matches) => Page(argument: match), + /// ) + /// ``` + final PathWidgetBuilder builder; + + /// If the route should open on the second screen on foldables. + final bool openInSecondScreen; +} + +class RouteConfiguration { + /// List of [Path] to for route matching. When a named route is pushed with + /// [Navigator.pushNamed], the route name is matched with the [Path.pattern] + /// in the list below. As soon as there is a match, the associated builder + /// will be returned. This means that the paths higher up in the list will + /// take priority. + static List paths = [ + Path( + r'^' + DemoPage.baseRoute + r'/([\w-]+)$', + (BuildContext context, String? match) => DemoPage(slug: match), + ), + Path( + r'^' + rally_routes.homeRoute, + (BuildContext context, String? match) => StudyWrapper( + study: DeferredWidget(rally.loadLibrary, + () => rally.RallyApp()), // ignore: prefer_const_constructors + ), + openInSecondScreen: true, + ), + Path( + r'^' + shrine_routes.homeRoute, + (BuildContext context, String? match) => StudyWrapper( + study: DeferredWidget(shrine.loadLibrary, + () => shrine.ShrineApp()), // ignore: prefer_const_constructors + ), + openInSecondScreen: true, + ), + Path( + r'^' + crane_routes.defaultRoute, + (BuildContext context, String? match) => StudyWrapper( + study: DeferredWidget(crane.loadLibrary, + () => crane.CraneApp(), // ignore: prefer_const_constructors + placeholder: const DeferredLoadingPlaceholder(name: 'Crane')), + ), + openInSecondScreen: true, + ), + Path( + r'^' + fortnightly_routes.defaultRoute, + (BuildContext context, String? match) => StudyWrapper( + study: DeferredWidget( + fortnightly.loadLibrary, + // ignore: prefer_const_constructors + () => fortnightly.FortnightlyApp()), + ), + openInSecondScreen: true, + ), + Path( + r'^' + reply_routes.homeRoute, + // ignore: prefer_const_constructors + (BuildContext context, String? match) => + const StudyWrapper(study: reply.ReplyApp(), hasBottomNavBar: true), + openInSecondScreen: true, + ), + Path( + r'^' + starter_app_routes.defaultRoute, + (BuildContext context, String? match) => const StudyWrapper( + study: starter_app.StarterApp(), + ), + openInSecondScreen: true, + ), + Path( + r'^/', + (BuildContext context, String? match) => const RootPage(), + ), + ]; + + /// The route generator callback used when the app is navigated to a named + /// route. Set it on the [MaterialApp.onGenerateRoute] or + /// [WidgetsApp.onGenerateRoute] to make use of the [paths] for route + /// matching. + static Route? onGenerateRoute( + RouteSettings settings, + bool hasHinge, + ) { + for (final Path path in paths) { + final RegExp regExpPattern = RegExp(path.pattern); + if (regExpPattern.hasMatch(settings.name!)) { + final RegExpMatch firstMatch = regExpPattern.firstMatch(settings.name!)!; + final String? match = (firstMatch.groupCount == 1) ? firstMatch.group(1) : null; + if (kIsWeb) { + return NoAnimationMaterialPageRoute( + builder: (BuildContext context) => path.builder(context, match), + settings: settings, + ); + } + if (path.openInSecondScreen && hasHinge) { + return TwoPanePageRoute( + builder: (BuildContext context) => path.builder(context, match), + settings: settings, + ); + } else { + return MaterialPageRoute( + builder: (BuildContext context) => path.builder(context, match), + settings: settings, + ); + } + } + } + + // If no match was found, we let [WidgetsApp.onUnknownRoute] handle it. + return null; + } +} + +class NoAnimationMaterialPageRoute extends MaterialPageRoute { + NoAnimationMaterialPageRoute({ + required super.builder, + super.settings, + }); + + @override + Widget buildTransitions( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return child; + } +} + +class TwoPanePageRoute extends OverlayRoute { + TwoPanePageRoute({ + required this.builder, + super.settings, + }); + + final WidgetBuilder builder; + + @override + Iterable createOverlayEntries() sync* { + yield OverlayEntry(builder: (BuildContext context) { + final Rect? hinge = MediaQuery.of(context).hinge?.bounds; + if (hinge == null) { + return builder.call(context); + } else { + return Positioned( + top: 0, + left: hinge.right, + right: 0, + bottom: 0, + child: builder.call(context)); + } + }); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/crane/app.dart b/dev/integration_tests/new_gallery/lib/studies/crane/app.dart new file mode 100644 index 0000000000..c576b734ef --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/crane/app.dart @@ -0,0 +1,60 @@ +// 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:flutter/material.dart'; + +import '../../data/gallery_options.dart'; +import '../../gallery_localizations.dart'; +import 'backdrop.dart'; +import 'backlayer.dart'; +import 'eat_form.dart'; +import 'fly_form.dart'; +import 'routes.dart' as routes; +import 'sleep_form.dart'; +import 'theme.dart'; + +class CraneApp extends StatelessWidget { + const CraneApp({super.key}); + + static const String defaultRoute = routes.defaultRoute; + + @override + Widget build(BuildContext context) { + return MaterialApp( + restorationScopeId: 'crane_app', + title: 'Crane', + debugShowCheckedModeBanner: false, + localizationsDelegates: GalleryLocalizations.localizationsDelegates, + supportedLocales: GalleryLocalizations.supportedLocales, + locale: GalleryOptions.of(context).locale, + initialRoute: CraneApp.defaultRoute, + routes: { + CraneApp.defaultRoute: (BuildContext context) => const _Home(), + }, + theme: craneTheme.copyWith( + platform: GalleryOptions.of(context).platform, + ), + ); + } +} + +class _Home extends StatelessWidget { + const _Home(); + + @override + Widget build(BuildContext context) { + return const ApplyTextOptions( + child: Backdrop( + frontLayer: SizedBox(), + backLayerItems: [ + FlyForm(), + SleepForm(), + EatForm(), + ], + frontTitle: Text('CRANE'), + backTitle: Text('MENU'), + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/crane/backdrop.dart b/dev/integration_tests/new_gallery/lib/studies/crane/backdrop.dart new file mode 100644 index 0000000000..cfa9b6e074 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/crane/backdrop.dart @@ -0,0 +1,390 @@ +// 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:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; + +import '../../data/gallery_options.dart'; +import '../../gallery_localizations.dart'; +import '../../layout/adaptive.dart'; +import '../../layout/image_placeholder.dart'; +import 'backlayer.dart'; +import 'border_tab_indicator.dart'; +import 'colors.dart'; +import 'header_form.dart'; +import 'item_cards.dart'; +import 'model/data.dart'; +import 'model/destination.dart'; + +class _FrontLayer extends StatefulWidget { + const _FrontLayer({ + required this.title, + required this.index, + required this.mobileTopOffset, + required this.restorationId, + }); + + final String title; + final int index; + final double mobileTopOffset; + final String restorationId; + + @override + _FrontLayerState createState() => _FrontLayerState(); +} + +class _FrontLayerState extends State<_FrontLayer> { + List? destinations; + + static const double frontLayerBorderRadius = 16.0; + static const EdgeInsets bottomPadding = EdgeInsets.only(bottom: 120); + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // We use didChangeDependencies because the initialization involves an + // InheritedWidget (for localization). However, we don't need to get + // destinations again when, say, resizing the window. + if (destinations == null) { + if (widget.index == 0) { + destinations = getFlyDestinations(context); + } + if (widget.index == 1) { + destinations = getSleepDestinations(context); + } + if (widget.index == 2) { + destinations = getEatDestinations(context); + } + } + } + + Widget _header() { + return Align( + alignment: AlignmentDirectional.centerStart, + child: Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 22, + ), + child: SelectableText( + widget.title, + style: Theme.of(context).textTheme.titleSmall, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + final bool isSmallDesktop = isDisplaySmallDesktop(context); + final int crossAxisCount = isDesktop ? 4 : 1; + + return FocusTraversalGroup( + policy: ReadingOrderTraversalPolicy(), + child: Padding( + padding: isDesktop + ? EdgeInsets.zero + : EdgeInsets.only(top: widget.mobileTopOffset), + child: PhysicalShape( + elevation: 16, + color: cranePrimaryWhite, + clipper: const ShapeBorderClipper( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(frontLayerBorderRadius), + topRight: Radius.circular(frontLayerBorderRadius), + ), + ), + ), + child: Padding( + padding: isDesktop + ? EdgeInsets.symmetric( + horizontal: + isSmallDesktop ? appPaddingSmall : appPaddingLarge) + .add(bottomPadding) + : const EdgeInsets.symmetric(horizontal: 20).add(bottomPadding), + child: Column( + children: [ + _header(), + Expanded( + child: MasonryGridView.count( + key: ValueKey('CraneListView-${widget.index}'), + restorationId: widget.restorationId, + crossAxisCount: crossAxisCount, + crossAxisSpacing: 16.0, + itemBuilder: (BuildContext context, int index) => + DestinationCard(destination: destinations![index]), + itemCount: destinations!.length, + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +/// Builds a Backdrop. +/// +/// A Backdrop widget has two layers, front and back. The front layer is shown +/// by default, and slides down to show the back layer, from which a user +/// can make a selection. The user can also configure the titles for when the +/// front or back layer is showing. +class Backdrop extends StatefulWidget { + + const Backdrop({ + super.key, + required this.frontLayer, + required this.backLayerItems, + required this.frontTitle, + required this.backTitle, + }); + final Widget frontLayer; + final List backLayerItems; + final Widget frontTitle; + final Widget backTitle; + + @override + State createState() => _BackdropState(); +} + +class _BackdropState extends State + with TickerProviderStateMixin, RestorationMixin { + final RestorableInt tabIndex = RestorableInt(0); + late TabController _tabController; + late Animation _flyLayerHorizontalOffset; + late Animation _sleepLayerHorizontalOffset; + late Animation _eatLayerHorizontalOffset; + + // How much the 'sleep' front layer is vertically offset relative to other + // front layers, in pixels, with the mobile layout. + static const double _sleepLayerTopOffset = 60.0; + + @override + String get restorationId => 'tab_non_scrollable_demo'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(tabIndex, 'tab_index'); + _tabController.index = tabIndex.value; + } + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 3, vsync: this); + _tabController.addListener(() { + // When the tab controller's value is updated, make sure to update the + // tab index value, which is state restorable. + setState(() { + tabIndex.value = _tabController.index; + }); + }); + + // Offsets to create a horizontal gap between front layers. + final Animation tabControllerAnimation = _tabController.animation!; + + _flyLayerHorizontalOffset = tabControllerAnimation.drive( + Tween(begin: Offset.zero, end: const Offset(-0.05, 0))); + + _sleepLayerHorizontalOffset = tabControllerAnimation.drive( + Tween(begin: const Offset(0.05, 0), end: Offset.zero)); + + _eatLayerHorizontalOffset = tabControllerAnimation.drive(Tween( + begin: const Offset(0.10, 0), end: const Offset(0.05, 0))); + } + + @override + void dispose() { + _tabController.dispose(); + tabIndex.dispose(); + super.dispose(); + } + + void _handleTabs(int tabIndex) { + _tabController.animateTo(tabIndex, + duration: const Duration(milliseconds: 300)); + } + + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + final double textScaleFactor = GalleryOptions.of(context).textScaleFactor(context); + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return Material( + color: cranePurple800, + child: Padding( + padding: const EdgeInsets.only(top: 12), + child: FocusTraversalGroup( + policy: ReadingOrderTraversalPolicy(), + child: Scaffold( + backgroundColor: cranePurple800, + appBar: AppBar( + automaticallyImplyLeading: false, + systemOverlayStyle: SystemUiOverlayStyle.light, + elevation: 0, + titleSpacing: 0, + flexibleSpace: CraneAppBar( + tabController: _tabController, + tabHandler: _handleTabs, + ), + ), + body: Stack( + children: [ + BackLayer( + tabController: _tabController, + backLayerItems: widget.backLayerItems, + ), + Container( + margin: EdgeInsets.only( + top: isDesktop + ? (isDisplaySmallDesktop(context) + ? textFieldHeight * 3 + : textFieldHeight * 2) + + 20 * textScaleFactor / 2 + : 175 + 140 * textScaleFactor / 2, + ), + // To display the middle front layer higher than the others, + // we allow the TabBarView to overflow by an offset + // (doubled because it technically overflows top & bottom). + // The other front layers are top padded by this offset. + child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { + return OverflowBox( + maxHeight: + constraints.maxHeight + _sleepLayerTopOffset * 2, + child: TabBarView( + physics: isDesktop + ? const NeverScrollableScrollPhysics() + : null, // use default TabBarView physics + controller: _tabController, + children: [ + SlideTransition( + position: _flyLayerHorizontalOffset, + child: _FrontLayer( + title: localizations.craneFlySubhead, + index: 0, + mobileTopOffset: _sleepLayerTopOffset, + restorationId: 'fly-subhead', + ), + ), + SlideTransition( + position: _sleepLayerHorizontalOffset, + child: _FrontLayer( + title: localizations.craneSleepSubhead, + index: 1, + mobileTopOffset: 0, + restorationId: 'sleep-subhead', + ), + ), + SlideTransition( + position: _eatLayerHorizontalOffset, + child: _FrontLayer( + title: localizations.craneEatSubhead, + index: 2, + mobileTopOffset: _sleepLayerTopOffset, + restorationId: 'eat-subhead', + ), + ), + ], + ), + ); + }), + ), + ], + ), + ), + ), + ), + ); + } +} + +class CraneAppBar extends StatefulWidget { + + const CraneAppBar({ + super.key, + this.tabHandler, + required this.tabController, + }); + final void Function(int)? tabHandler; + final TabController tabController; + + @override + State createState() => _CraneAppBarState(); +} + +class _CraneAppBarState extends State { + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + final bool isSmallDesktop = isDisplaySmallDesktop(context); + final double textScaleFactor = GalleryOptions.of(context).textScaleFactor(context); + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + return SafeArea( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: + isDesktop && !isSmallDesktop ? appPaddingLarge : appPaddingSmall, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const ExcludeSemantics( + child: FadeInImagePlaceholder( + image: AssetImage( + 'crane/logo/logo.png', + package: 'flutter_gallery_assets', + ), + placeholder: SizedBox( + width: 40, + height: 60, + ), + width: 40, + height: 60, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsetsDirectional.only(start: 24), + child: Theme( + data: Theme.of(context).copyWith( + splashColor: Colors.transparent, + ), + child: TabBar( + indicator: BorderTabIndicator( + indicatorHeight: isDesktop ? 28 : 32, + textScaleFactor: textScaleFactor, + ), + controller: widget.tabController, + labelPadding: const EdgeInsets.symmetric(horizontal: 32), + isScrollable: true, + // left-align tabs on desktop + labelStyle: Theme.of(context).textTheme.labelLarge, + labelColor: cranePrimaryWhite, + physics: const BouncingScrollPhysics(), + unselectedLabelColor: cranePrimaryWhite.withOpacity(.6), + onTap: (int index) => widget.tabController.animateTo( + index, + duration: const Duration(milliseconds: 300), + ), + tabs: [ + Tab(text: localizations.craneFly), + Tab(text: localizations.craneSleep), + Tab(text: localizations.craneEat), + ], + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/crane/backlayer.dart b/dev/integration_tests/new_gallery/lib/studies/crane/backlayer.dart new file mode 100644 index 0000000000..14eb6db2b4 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/crane/backlayer.dart @@ -0,0 +1,48 @@ +// 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:flutter/material.dart'; + +abstract class BackLayerItem extends StatefulWidget { + + const BackLayerItem({super.key, required this.index}); + final int index; +} + +class BackLayer extends StatefulWidget { + + const BackLayer({ + super.key, + required this.backLayerItems, + required this.tabController, + }); + final List backLayerItems; + final TabController tabController; + + @override + State createState() => _BackLayerState(); +} + +class _BackLayerState extends State { + @override + void initState() { + super.initState(); + widget.tabController.addListener(() => setState(() {})); + } + + @override + Widget build(BuildContext context) { + final int tabIndex = widget.tabController.index; + return IndexedStack( + index: tabIndex, + children: [ + for (final BackLayerItem backLayerItem in widget.backLayerItems) + ExcludeFocus( + excluding: backLayerItem.index != tabIndex, + child: backLayerItem, + ) + ], + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/crane/border_tab_indicator.dart b/dev/integration_tests/new_gallery/lib/studies/crane/border_tab_indicator.dart new file mode 100644 index 0000000000..7b5eb05598 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/crane/border_tab_indicator.dart @@ -0,0 +1,51 @@ +// 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:flutter/material.dart'; + +class BorderTabIndicator extends Decoration { + const BorderTabIndicator({ + required this.indicatorHeight, + required this.textScaleFactor, + }) : super(); + + final double indicatorHeight; + final double textScaleFactor; + + @override + BorderPainter createBoxPainter([VoidCallback? onChanged]) { + return BorderPainter(this, indicatorHeight, textScaleFactor, onChanged); + } +} + +class BorderPainter extends BoxPainter { + BorderPainter( + this.decoration, + this.indicatorHeight, + this.textScaleFactor, + VoidCallback? onChanged, + ) : assert(indicatorHeight >= 0), + super(onChanged); + + final BorderTabIndicator decoration; + final double indicatorHeight; + final double textScaleFactor; + + @override + void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { + assert(configuration.size != null); + final double horizontalInset = 16 - 4 * textScaleFactor; + final Rect rect = Offset(offset.dx + horizontalInset, + (configuration.size!.height / 2) - indicatorHeight / 2 - 1) & + Size(configuration.size!.width - 2 * horizontalInset, indicatorHeight); + final Paint paint = Paint(); + paint.color = Colors.white; + paint.style = PaintingStyle.stroke; + paint.strokeWidth = 2; + canvas.drawRRect( + RRect.fromRectAndRadius(rect, const Radius.circular(56)), + paint, + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/crane/colors.dart b/dev/integration_tests/new_gallery/lib/studies/crane/colors.dart new file mode 100644 index 0000000000..d6f69baab5 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/crane/colors.dart @@ -0,0 +1,20 @@ +// 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:flutter/material.dart'; + +const Color cranePurple700 = Color(0xFF720D5D); +const Color cranePurple800 = Color(0xFF5D1049); +const Color cranePurple900 = Color(0xFF4E0D3A); + +const Color craneRed700 = Color(0xFFE30425); + +const Color craneWhite60 = Color(0x99FFFFFF); +const Color cranePrimaryWhite = Color(0xFFFFFFFF); +const Color craneErrorOrange = Color(0xFFFF9100); + +const Color craneAlpha = Color(0x00FFFFFF); + +const Color craneGrey = Color(0xFF747474); +const Color craneBlack = Color(0xFF1E252D); diff --git a/dev/integration_tests/new_gallery/lib/studies/crane/eat_form.dart b/dev/integration_tests/new_gallery/lib/studies/crane/eat_form.dart new file mode 100644 index 0000000000..097727bacb --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/crane/eat_form.dart @@ -0,0 +1,76 @@ +// 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:flutter/material.dart'; + +import '../../gallery_localizations.dart'; +import 'backlayer.dart'; +import 'header_form.dart'; + +class EatForm extends BackLayerItem { + const EatForm({super.key}) : super(index: 2); + + @override + State createState() => _EatFormState(); +} + +class _EatFormState extends State with RestorationMixin { + final RestorableTextEditingController dinerController = RestorableTextEditingController(); + final RestorableTextEditingController dateController = RestorableTextEditingController(); + final RestorableTextEditingController timeController = RestorableTextEditingController(); + final RestorableTextEditingController locationController = RestorableTextEditingController(); + + @override + String get restorationId => 'eat_form'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(dinerController, 'diner_controller'); + registerForRestoration(dateController, 'date_controller'); + registerForRestoration(timeController, 'time_controller'); + registerForRestoration(locationController, 'location_controller'); + } + + @override + void dispose() { + dinerController.dispose(); + dateController.dispose(); + timeController.dispose(); + locationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return HeaderForm( + fields: [ + HeaderFormField( + index: 0, + iconData: Icons.person, + title: localizations.craneFormDiners, + textController: dinerController.value, + ), + HeaderFormField( + index: 1, + iconData: Icons.date_range, + title: localizations.craneFormDate, + textController: dateController.value, + ), + HeaderFormField( + index: 2, + iconData: Icons.access_time, + title: localizations.craneFormTime, + textController: timeController.value, + ), + HeaderFormField( + index: 3, + iconData: Icons.restaurant_menu, + title: localizations.craneFormLocation, + textController: locationController.value, + ), + ], + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/crane/fly_form.dart b/dev/integration_tests/new_gallery/lib/studies/crane/fly_form.dart new file mode 100644 index 0000000000..9a69842acd --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/crane/fly_form.dart @@ -0,0 +1,76 @@ +// 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:flutter/material.dart'; + +import '../../gallery_localizations.dart'; +import 'backlayer.dart'; +import 'header_form.dart'; + +class FlyForm extends BackLayerItem { + const FlyForm({super.key}) : super(index: 0); + + @override + State createState() => _FlyFormState(); +} + +class _FlyFormState extends State with RestorationMixin { + final RestorableTextEditingController travelerController = RestorableTextEditingController(); + final RestorableTextEditingController countryDestinationController = RestorableTextEditingController(); + final RestorableTextEditingController destinationController = RestorableTextEditingController(); + final RestorableTextEditingController dateController = RestorableTextEditingController(); + + @override + String get restorationId => 'fly_form'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(travelerController, 'diner_controller'); + registerForRestoration(countryDestinationController, 'date_controller'); + registerForRestoration(destinationController, 'time_controller'); + registerForRestoration(dateController, 'location_controller'); + } + + @override + void dispose() { + travelerController.dispose(); + countryDestinationController.dispose(); + destinationController.dispose(); + dateController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return HeaderForm( + fields: [ + HeaderFormField( + index: 0, + iconData: Icons.person, + title: localizations.craneFormTravelers, + textController: travelerController.value, + ), + HeaderFormField( + index: 1, + iconData: Icons.place, + title: localizations.craneFormOrigin, + textController: countryDestinationController.value, + ), + HeaderFormField( + index: 2, + iconData: Icons.airplanemode_active, + title: localizations.craneFormDestination, + textController: destinationController.value, + ), + HeaderFormField( + index: 3, + iconData: Icons.date_range, + title: localizations.craneFormDates, + textController: dateController.value, + ), + ], + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/crane/header_form.dart b/dev/integration_tests/new_gallery/lib/studies/crane/header_form.dart new file mode 100644 index 0000000000..70d5202671 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/crane/header_form.dart @@ -0,0 +1,110 @@ +// 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:flutter/material.dart'; +import '../../layout/adaptive.dart'; +import 'colors.dart'; + +const double textFieldHeight = 60.0; +const double appPaddingLarge = 120.0; +const double appPaddingSmall = 24.0; + +class HeaderFormField { + + const HeaderFormField({ + required this.index, + required this.iconData, + required this.title, + required this.textController, + }); + final int index; + final IconData iconData; + final String title; + final TextEditingController textController; +} + +class HeaderForm extends StatelessWidget { + + const HeaderForm({super.key, required this.fields}); + final List fields; + + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + final bool isSmallDesktop = isDisplaySmallDesktop(context); + + return Padding( + padding: EdgeInsets.symmetric( + horizontal: + isDesktop && !isSmallDesktop ? appPaddingLarge : appPaddingSmall, + ), + child: isDesktop + ? LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { + int crossAxisCount = isSmallDesktop ? 2 : 4; + if (fields.length < crossAxisCount) { + crossAxisCount = fields.length; + } + final double itemWidth = constraints.maxWidth / crossAxisCount; + return GridView.count( + crossAxisCount: crossAxisCount, + childAspectRatio: itemWidth / textFieldHeight, + physics: const NeverScrollableScrollPhysics(), + children: [ + for (final HeaderFormField field in fields) + if ((field.index + 1) % crossAxisCount == 0) + _HeaderTextField(field: field) + else + Padding( + padding: const EdgeInsetsDirectional.only(end: 16), + child: _HeaderTextField(field: field), + ), + ], + ); + }) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (final HeaderFormField field in fields) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _HeaderTextField(field: field), + ) + ], + ), + ); + } +} + +class _HeaderTextField extends StatelessWidget { + + const _HeaderTextField({required this.field}); + final HeaderFormField field; + + @override + Widget build(BuildContext context) { + return TextField( + controller: field.textController, + cursorColor: Theme.of(context).colorScheme.secondary, + style: + Theme.of(context).textTheme.bodyLarge!.copyWith(color: Colors.white), + onTap: () {}, + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(4), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.all(16), + fillColor: cranePurple700, + filled: true, + hintText: field.title, + floatingLabelBehavior: FloatingLabelBehavior.never, + prefixIcon: Icon( + field.iconData, + size: 24, + color: Theme.of(context).iconTheme.color, + ), + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/crane/item_cards.dart b/dev/integration_tests/new_gallery/lib/studies/crane/item_cards.dart new file mode 100644 index 0000000000..dc926f6cc7 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/crane/item_cards.dart @@ -0,0 +1,122 @@ +// 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:flutter/material.dart'; +import '../../layout/adaptive.dart'; +import '../../layout/highlight_focus.dart'; +import '../../layout/image_placeholder.dart'; +import 'model/destination.dart'; + +// Width and height for thumbnail images. +const double mobileThumbnailSize = 60.0; + +class DestinationCard extends StatelessWidget { + const DestinationCard({ + super.key, + required this.destination, + }); + + final Destination destination; + + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + final TextTheme textTheme = Theme.of(context).textTheme; + + final Widget card = isDesktop + ? Padding( + padding: const EdgeInsets.only(bottom: 40), + child: Semantics( + container: true, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(4)), + child: _DestinationImage(destination: destination), + ), + Padding( + padding: const EdgeInsets.only(top: 20, bottom: 10), + child: SelectableText( + destination.destination, + style: textTheme.titleMedium, + ), + ), + SelectableText( + destination.subtitle(context), + semanticsLabel: destination.subtitleSemantics(context), + style: textTheme.titleSmall, + ), + ], + ), + ), + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + contentPadding: const EdgeInsetsDirectional.only(end: 8), + leading: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(4)), + child: SizedBox( + width: mobileThumbnailSize, + height: mobileThumbnailSize, + child: _DestinationImage(destination: destination), + ), + ), + title: SelectableText(destination.destination, + style: textTheme.titleMedium), + subtitle: SelectableText( + destination.subtitle(context), + semanticsLabel: destination.subtitleSemantics(context), + style: textTheme.titleSmall, + ), + ), + const Divider(thickness: 1), + ], + ); + + return HighlightFocus( + debugLabel: 'DestinationCard: ${destination.destination}', + highlightColor: Colors.red.withOpacity(0.1), + onPressed: () {}, + child: card, + ); + } +} + +class _DestinationImage extends StatelessWidget { + const _DestinationImage({ + required this.destination, + }); + + final Destination destination; + + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + + return Semantics( + label: destination.assetSemanticLabel, + child: ExcludeSemantics( + child: FadeInImagePlaceholder( + image: AssetImage( + destination.assetName, + package: 'flutter_gallery_assets', + ), + fit: BoxFit.cover, + width: isDesktop ? null : mobileThumbnailSize, + height: isDesktop ? null : mobileThumbnailSize, + placeholder: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { + return Container( + color: Colors.black.withOpacity(0.1), + width: constraints.maxWidth, + height: constraints.maxWidth / destination.imageAspectRatio, + ); + }), + ), + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/crane/model/data.dart b/dev/integration_tests/new_gallery/lib/studies/crane/model/data.dart new file mode 100644 index 0000000000..9ea2105069 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/crane/model/data.dart @@ -0,0 +1,296 @@ +// 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:flutter/material.dart'; +import '../../../gallery_localizations.dart'; +import 'destination.dart'; + +List getFlyDestinations(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return [ + FlyDestination( + id: 0, + destination: localizations.craneFly0, + stops: 1, + duration: const Duration(hours: 6, minutes: 15), + assetSemanticLabel: localizations.craneFly0SemanticLabel, + ), + FlyDestination( + id: 1, + destination: localizations.craneFly1, + stops: 0, + duration: const Duration(hours: 13, minutes: 30), + assetSemanticLabel: localizations.craneFly1SemanticLabel, + imageAspectRatio: 400 / 410, + ), + FlyDestination( + id: 2, + destination: localizations.craneFly2, + stops: 0, + duration: const Duration(hours: 5, minutes: 16), + assetSemanticLabel: localizations.craneFly2SemanticLabel, + imageAspectRatio: 400 / 394, + ), + FlyDestination( + id: 3, + destination: localizations.craneFly3, + stops: 2, + duration: const Duration(hours: 19, minutes: 40), + assetSemanticLabel: localizations.craneFly3SemanticLabel, + imageAspectRatio: 400 / 377, + ), + FlyDestination( + id: 4, + destination: localizations.craneFly4, + stops: 0, + duration: const Duration(hours: 8, minutes: 24), + assetSemanticLabel: localizations.craneFly4SemanticLabel, + imageAspectRatio: 400 / 308, + ), + FlyDestination( + id: 5, + destination: localizations.craneFly5, + stops: 1, + duration: const Duration(hours: 14, minutes: 12), + assetSemanticLabel: localizations.craneFly5SemanticLabel, + imageAspectRatio: 400 / 418, + ), + FlyDestination( + id: 6, + destination: localizations.craneFly6, + stops: 0, + duration: const Duration(hours: 5, minutes: 24), + assetSemanticLabel: localizations.craneFly6SemanticLabel, + imageAspectRatio: 400 / 345, + ), + FlyDestination( + id: 7, + destination: localizations.craneFly7, + stops: 1, + duration: const Duration(hours: 5, minutes: 43), + assetSemanticLabel: localizations.craneFly7SemanticLabel, + imageAspectRatio: 400 / 408, + ), + FlyDestination( + id: 8, + destination: localizations.craneFly8, + stops: 0, + duration: const Duration(hours: 8, minutes: 25), + assetSemanticLabel: localizations.craneFly8SemanticLabel, + imageAspectRatio: 400 / 399, + ), + FlyDestination( + id: 9, + destination: localizations.craneFly9, + stops: 1, + duration: const Duration(hours: 15, minutes: 52), + assetSemanticLabel: localizations.craneFly9SemanticLabel, + imageAspectRatio: 400 / 379, + ), + FlyDestination( + id: 10, + destination: localizations.craneFly10, + stops: 0, + duration: const Duration(hours: 5, minutes: 57), + assetSemanticLabel: localizations.craneFly10SemanticLabel, + imageAspectRatio: 400 / 307, + ), + FlyDestination( + id: 11, + destination: localizations.craneFly11, + stops: 1, + duration: const Duration(hours: 13, minutes: 24), + assetSemanticLabel: localizations.craneFly11SemanticLabel, + imageAspectRatio: 400 / 369, + ), + FlyDestination( + id: 12, + destination: localizations.craneFly12, + stops: 2, + duration: const Duration(hours: 10, minutes: 20), + assetSemanticLabel: localizations.craneFly12SemanticLabel, + imageAspectRatio: 400 / 394, + ), + FlyDestination( + id: 13, + destination: localizations.craneFly13, + stops: 0, + duration: const Duration(hours: 7, minutes: 15), + assetSemanticLabel: localizations.craneFly13SemanticLabel, + imageAspectRatio: 400 / 433, + ), + ]; +} + +List getSleepDestinations(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return [ + SleepDestination( + id: 0, + destination: localizations.craneSleep0, + total: 2241, + assetSemanticLabel: localizations.craneSleep0SemanticLabel, + imageAspectRatio: 400 / 308, + ), + SleepDestination( + id: 1, + destination: localizations.craneSleep1, + total: 876, + assetSemanticLabel: localizations.craneSleep1SemanticLabel, + ), + SleepDestination( + id: 2, + destination: localizations.craneSleep2, + total: 1286, + assetSemanticLabel: localizations.craneSleep2SemanticLabel, + imageAspectRatio: 400 / 377, + ), + SleepDestination( + id: 3, + destination: localizations.craneSleep3, + total: 496, + assetSemanticLabel: localizations.craneSleep3SemanticLabel, + imageAspectRatio: 400 / 379, + ), + SleepDestination( + id: 4, + destination: localizations.craneSleep4, + total: 390, + assetSemanticLabel: localizations.craneSleep4SemanticLabel, + imageAspectRatio: 400 / 418, + ), + SleepDestination( + id: 5, + destination: localizations.craneSleep5, + total: 876, + assetSemanticLabel: localizations.craneSleep5SemanticLabel, + imageAspectRatio: 400 / 410, + ), + SleepDestination( + id: 6, + destination: localizations.craneSleep6, + total: 989, + assetSemanticLabel: localizations.craneSleep6SemanticLabel, + imageAspectRatio: 400 / 394, + ), + SleepDestination( + id: 7, + destination: localizations.craneSleep7, + total: 306, + assetSemanticLabel: localizations.craneSleep7SemanticLabel, + imageAspectRatio: 400 / 266, + ), + SleepDestination( + id: 8, + destination: localizations.craneSleep8, + total: 385, + assetSemanticLabel: localizations.craneSleep8SemanticLabel, + imageAspectRatio: 400 / 376, + ), + SleepDestination( + id: 9, + destination: localizations.craneSleep9, + total: 989, + assetSemanticLabel: localizations.craneSleep9SemanticLabel, + imageAspectRatio: 400 / 369, + ), + SleepDestination( + id: 10, + destination: localizations.craneSleep10, + total: 1380, + assetSemanticLabel: localizations.craneSleep10SemanticLabel, + imageAspectRatio: 400 / 307, + ), + SleepDestination( + id: 11, + destination: localizations.craneSleep11, + total: 1109, + assetSemanticLabel: localizations.craneSleep11SemanticLabel, + imageAspectRatio: 400 / 456, + ), + ]; +} + +List getEatDestinations(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return [ + EatDestination( + id: 0, + destination: localizations.craneEat0, + total: 354, + assetSemanticLabel: localizations.craneEat0SemanticLabel, + imageAspectRatio: 400 / 444, + ), + EatDestination( + id: 1, + destination: localizations.craneEat1, + total: 623, + assetSemanticLabel: localizations.craneEat1SemanticLabel, + imageAspectRatio: 400 / 340, + ), + EatDestination( + id: 2, + destination: localizations.craneEat2, + total: 124, + assetSemanticLabel: localizations.craneEat2SemanticLabel, + imageAspectRatio: 400 / 406, + ), + EatDestination( + id: 3, + destination: localizations.craneEat3, + total: 495, + assetSemanticLabel: localizations.craneEat3SemanticLabel, + imageAspectRatio: 400 / 323, + ), + EatDestination( + id: 4, + destination: localizations.craneEat4, + total: 683, + assetSemanticLabel: localizations.craneEat4SemanticLabel, + imageAspectRatio: 400 / 404, + ), + EatDestination( + id: 5, + destination: localizations.craneEat5, + total: 786, + assetSemanticLabel: localizations.craneEat5SemanticLabel, + imageAspectRatio: 400 / 407, + ), + EatDestination( + id: 6, + destination: localizations.craneEat6, + total: 323, + assetSemanticLabel: localizations.craneEat6SemanticLabel, + imageAspectRatio: 400 / 431, + ), + EatDestination( + id: 7, + destination: localizations.craneEat7, + total: 285, + assetSemanticLabel: localizations.craneEat7SemanticLabel, + imageAspectRatio: 400 / 422, + ), + EatDestination( + id: 8, + destination: localizations.craneEat8, + total: 323, + assetSemanticLabel: localizations.craneEat8SemanticLabel, + imageAspectRatio: 400 / 300, + ), + EatDestination( + id: 9, + destination: localizations.craneEat9, + total: 1406, + assetSemanticLabel: localizations.craneEat9SemanticLabel, + imageAspectRatio: 400 / 451, + ), + EatDestination( + id: 10, + destination: localizations.craneEat10, + total: 849, + assetSemanticLabel: localizations.craneEat10SemanticLabel, + imageAspectRatio: 400 / 266, + ), + ]; +} diff --git a/dev/integration_tests/new_gallery/lib/studies/crane/model/destination.dart b/dev/integration_tests/new_gallery/lib/studies/crane/model/destination.dart new file mode 100644 index 0000000000..ac09c42438 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/crane/model/destination.dart @@ -0,0 +1,118 @@ +// 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:flutter/material.dart'; + +import '../../../data/gallery_options.dart'; +import '../../../gallery_localizations.dart'; +import 'formatters.dart'; + +abstract class Destination { + const Destination({ + required this.id, + required this.destination, + required this.assetSemanticLabel, + required this.imageAspectRatio, + }); + + final int id; + final String destination; + final String assetSemanticLabel; + final double imageAspectRatio; + + String get assetName; + + String subtitle(BuildContext context); + + String subtitleSemantics(BuildContext context) => subtitle(context); + + @override + String toString() => '$destination (id=$id)'; +} + +class FlyDestination extends Destination { + const FlyDestination({ + required super.id, + required super.destination, + required super.assetSemanticLabel, + required this.stops, + super.imageAspectRatio = 1, + this.duration, + }); + + final int stops; + final Duration? duration; + + @override + String get assetName => 'crane/destinations/fly_$id.jpg'; + + @override + String subtitle(BuildContext context) { + final String stopsText = GalleryLocalizations.of(context)!.craneFlyStops(stops); + + if (duration == null) { + return stopsText; + } else { + final TextDirection? textDirection = GalleryOptions.of(context).resolvedTextDirection(); + final String durationText = + formattedDuration(context, duration!, abbreviated: true); + return textDirection == TextDirection.ltr + ? '$stopsText · $durationText' + : '$durationText · $stopsText'; + } + } + + @override + String subtitleSemantics(BuildContext context) { + final String stopsText = GalleryLocalizations.of(context)!.craneFlyStops(stops); + + if (duration == null) { + return stopsText; + } else { + final String durationText = + formattedDuration(context, duration!, abbreviated: false); + return '$stopsText, $durationText'; + } + } +} + +class SleepDestination extends Destination { + const SleepDestination({ + required super.id, + required super.destination, + required super.assetSemanticLabel, + required this.total, + super.imageAspectRatio = 1, + }); + + final int total; + + @override + String get assetName => 'crane/destinations/sleep_$id.jpg'; + + @override + String subtitle(BuildContext context) { + return GalleryLocalizations.of(context)!.craneSleepProperties(total); + } +} + +class EatDestination extends Destination { + const EatDestination({ + required super.id, + required super.destination, + required super.assetSemanticLabel, + required this.total, + super.imageAspectRatio = 1, + }); + + final int total; + + @override + String get assetName => 'crane/destinations/eat_$id.jpg'; + + @override + String subtitle(BuildContext context) { + return GalleryLocalizations.of(context)!.craneEatRestaurants(total); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/crane/model/formatters.dart b/dev/integration_tests/new_gallery/lib/studies/crane/model/formatters.dart new file mode 100644 index 0000000000..cacc0f9636 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/crane/model/formatters.dart @@ -0,0 +1,16 @@ +// 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:flutter/material.dart'; +import '../../../gallery_localizations.dart'; + +// Duration of time (e.g. 16h 12m) +String formattedDuration(BuildContext context, Duration duration, + {bool? abbreviated}) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + final String hoursShortForm = localizations.craneHours(duration.inHours); + final String minutesShortForm = localizations.craneMinutes(duration.inMinutes % 60); + return localizations.craneFlightDuration(hoursShortForm, minutesShortForm); +} diff --git a/dev/integration_tests/new_gallery/lib/studies/crane/routes.dart b/dev/integration_tests/new_gallery/lib/studies/crane/routes.dart new file mode 100644 index 0000000000..6cf67d61d5 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/crane/routes.dart @@ -0,0 +1,5 @@ +// 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. + +const String defaultRoute = '/crane'; diff --git a/dev/integration_tests/new_gallery/lib/studies/crane/sleep_form.dart b/dev/integration_tests/new_gallery/lib/studies/crane/sleep_form.dart new file mode 100644 index 0000000000..31fc912f80 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/crane/sleep_form.dart @@ -0,0 +1,68 @@ +// 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:flutter/material.dart'; +import '../../gallery_localizations.dart'; + +import 'backlayer.dart'; +import 'header_form.dart'; + +class SleepForm extends BackLayerItem { + const SleepForm({super.key}) : super(index: 1); + + @override + State createState() => _SleepFormState(); +} + +class _SleepFormState extends State with RestorationMixin { + final RestorableTextEditingController travelerController = RestorableTextEditingController(); + final RestorableTextEditingController dateController = RestorableTextEditingController(); + final RestorableTextEditingController locationController = RestorableTextEditingController(); + + @override + String get restorationId => 'sleep_form'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(travelerController, 'diner_controller'); + registerForRestoration(dateController, 'date_controller'); + registerForRestoration(locationController, 'time_controller'); + } + + @override + void dispose() { + travelerController.dispose(); + dateController.dispose(); + locationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + return HeaderForm( + fields: [ + HeaderFormField( + index: 0, + iconData: Icons.person, + title: localizations.craneFormTravelers, + textController: travelerController.value, + ), + HeaderFormField( + index: 1, + iconData: Icons.date_range, + title: localizations.craneFormDates, + textController: dateController.value, + ), + HeaderFormField( + index: 2, + iconData: Icons.hotel, + title: localizations.craneFormLocation, + textController: locationController.value, + ), + ], + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/crane/theme.dart b/dev/integration_tests/new_gallery/lib/studies/crane/theme.dart new file mode 100644 index 0000000000..c6b4fc98ef --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/crane/theme.dart @@ -0,0 +1,102 @@ +// 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:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import '../../layout/letter_spacing.dart'; +import 'colors.dart'; + +final ThemeData craneTheme = _buildCraneTheme(); + +IconThemeData _customIconTheme(IconThemeData original, Color color) { + return original.copyWith(color: color); +} + +ThemeData _buildCraneTheme() { + final ThemeData base = ThemeData.light(); + + return base.copyWith( + colorScheme: const ColorScheme.light().copyWith( + primary: cranePurple800, + secondary: craneRed700, + error: craneErrorOrange, + ), + hintColor: craneWhite60, + indicatorColor: cranePrimaryWhite, + scaffoldBackgroundColor: cranePrimaryWhite, + cardColor: cranePrimaryWhite, + highlightColor: Colors.transparent, + textTheme: _buildCraneTextTheme(base.textTheme), + textSelectionTheme: const TextSelectionThemeData( + selectionColor: cranePurple700, + ), + primaryTextTheme: _buildCraneTextTheme(base.primaryTextTheme), + iconTheme: _customIconTheme(base.iconTheme, craneWhite60), + primaryIconTheme: _customIconTheme(base.iconTheme, cranePrimaryWhite), + ); +} + +TextTheme _buildCraneTextTheme(TextTheme base) { + return GoogleFonts.ralewayTextTheme( + base.copyWith( + displayLarge: base.displayLarge!.copyWith( + fontWeight: FontWeight.w300, + fontSize: 96, + ), + displayMedium: base.displayMedium!.copyWith( + fontWeight: FontWeight.w400, + fontSize: 60, + ), + displaySmall: base.displaySmall!.copyWith( + fontWeight: FontWeight.w600, + fontSize: 48, + ), + headlineMedium: base.headlineMedium!.copyWith( + fontWeight: FontWeight.w600, + fontSize: 34, + ), + headlineSmall: base.headlineSmall!.copyWith( + fontWeight: FontWeight.w600, + fontSize: 24, + ), + titleLarge: base.titleLarge!.copyWith( + fontWeight: FontWeight.w600, + fontSize: 20, + ), + titleMedium: base.titleMedium!.copyWith( + fontWeight: FontWeight.w500, + fontSize: 16, + letterSpacing: letterSpacingOrNone(0.5), + ), + titleSmall: base.titleSmall!.copyWith( + fontWeight: FontWeight.w600, + fontSize: 12, + color: craneGrey, + ), + bodyLarge: base.bodyLarge!.copyWith( + fontWeight: FontWeight.w500, + fontSize: 16, + ), + bodyMedium: base.bodyMedium!.copyWith( + fontWeight: FontWeight.w400, + fontSize: 14, + ), + labelLarge: base.labelLarge!.copyWith( + fontWeight: FontWeight.w600, + fontSize: 13, + letterSpacing: letterSpacingOrNone(0.8), + ), + bodySmall: base.bodySmall!.copyWith( + fontWeight: FontWeight.w500, + fontSize: 12, + color: craneGrey, + ), + labelSmall: base.labelSmall!.copyWith( + fontWeight: FontWeight.w600, + fontSize: 12, + ), + ), + ); +} diff --git a/dev/integration_tests/new_gallery/lib/studies/fortnightly/app.dart b/dev/integration_tests/new_gallery/lib/studies/fortnightly/app.dart new file mode 100644 index 0000000000..3aa46e9183 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/fortnightly/app.dart @@ -0,0 +1,181 @@ +// 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:flutter/material.dart'; + +import '../../data/gallery_options.dart'; +import '../../gallery_localizations.dart'; +import '../../layout/adaptive.dart'; +import '../../layout/image_placeholder.dart'; +import '../../layout/text_scale.dart'; +import 'routes.dart' as routes; +import 'shared.dart'; + +const String _fortnightlyTitle = 'Fortnightly'; + +class FortnightlyApp extends StatelessWidget { + const FortnightlyApp({super.key}); + + static const String defaultRoute = routes.defaultRoute; + + @override + Widget build(BuildContext context) { + final StatelessWidget home = isDisplayDesktop(context) + ? const _FortnightlyHomeDesktop() + : const _FortnightlyHomeMobile(); + return MaterialApp( + restorationScopeId: 'fortnightly_app', + title: _fortnightlyTitle, + debugShowCheckedModeBanner: false, + theme: buildTheme(context).copyWith( + platform: GalleryOptions.of(context).platform, + ), + home: ApplyTextOptions(child: home), + routes: { + FortnightlyApp.defaultRoute: (BuildContext context) => ApplyTextOptions(child: home), + }, + initialRoute: FortnightlyApp.defaultRoute, + // L10n settings. + localizationsDelegates: GalleryLocalizations.localizationsDelegates, + supportedLocales: GalleryLocalizations.supportedLocales, + locale: GalleryOptions.of(context).locale, + ); + } +} + +class _FortnightlyHomeMobile extends StatelessWidget { + const _FortnightlyHomeMobile(); + + @override + Widget build(BuildContext context) { + return Scaffold( + drawer: const Drawer( + child: SafeArea( + child: NavigationMenu(isCloseable: true), + ), + ), + appBar: AppBar( + automaticallyImplyLeading: false, + title: Semantics( + label: _fortnightlyTitle, + child: const FadeInImagePlaceholder( + image: AssetImage( + 'fortnightly/fortnightly_title.png', + package: 'flutter_gallery_assets', + ), + placeholder: SizedBox.shrink(), + excludeFromSemantics: true, + ), + ), + actions: [ + IconButton( + icon: const Icon(Icons.search), + tooltip: GalleryLocalizations.of(context)!.shrineTooltipSearch, + onPressed: () {}, + ), + ], + ), + body: SafeArea( + child: ListView( + restorationId: 'list_view', + children: [ + const HashtagBar(), + for (final Widget item in buildArticlePreviewItems(context)) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: item, + ), + ], + ), + ), + ); + } +} + +class _FortnightlyHomeDesktop extends StatelessWidget { + const _FortnightlyHomeDesktop(); + + @override + Widget build(BuildContext context) { + const double menuWidth = 200.0; + const SizedBox spacer = SizedBox(width: 20); + final double headerHeight = 40 * reducedTextScale(context); + + return Scaffold( + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + SizedBox( + height: headerHeight, + child: Row( + children: [ + Container( + width: menuWidth, + alignment: AlignmentDirectional.centerStart, + margin: const EdgeInsets.only(left: 12), + child: Semantics( + label: _fortnightlyTitle, + child: Image.asset( + 'fortnightly/fortnightly_title.png', + package: 'flutter_gallery_assets', + excludeFromSemantics: true, + ), + ), + ), + spacer, + const Flexible( + flex: 2, + child: HashtagBar(), + ), + spacer, + Flexible( + fit: FlexFit.tight, + child: Container( + alignment: AlignmentDirectional.centerEnd, + child: IconButton( + icon: const Icon(Icons.search), + tooltip: GalleryLocalizations.of(context)! + .shrineTooltipSearch, + onPressed: () {}, + ), + ), + ), + ], + ), + ), + Flexible( + child: Row( + children: [ + const SizedBox( + width: menuWidth, + child: NavigationMenu(), + ), + spacer, + Flexible( + flex: 2, + child: ListView( + children: buildArticlePreviewItems(context), + ), + ), + spacer, + Flexible( + fit: FlexFit.tight, + child: ListView( + children: [ + ...buildStockItems(context), + const SizedBox(height: 32), + ...buildVideoPreviewItems(context), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/fortnightly/routes.dart b/dev/integration_tests/new_gallery/lib/studies/fortnightly/routes.dart new file mode 100644 index 0000000000..1587de133d --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/fortnightly/routes.dart @@ -0,0 +1,5 @@ +// 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. + +const String defaultRoute = '/fortnightly'; diff --git a/dev/integration_tests/new_gallery/lib/studies/fortnightly/shared.dart b/dev/integration_tests/new_gallery/lib/studies/fortnightly/shared.dart new file mode 100644 index 0000000000..d8cc8b4709 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/fortnightly/shared.dart @@ -0,0 +1,621 @@ +// 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:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:intl/intl.dart'; + +import '../../data/gallery_options.dart'; +import '../../gallery_localizations.dart'; +import '../../layout/image_placeholder.dart'; +import '../../layout/text_scale.dart'; + +class ArticleData { + ArticleData({ + required this.imageUrl, + required this.imageAspectRatio, + required this.category, + required this.title, + this.snippet, + }); + + final String imageUrl; + final double imageAspectRatio; + final String category; + final String title; + final String? snippet; +} + +class HorizontalArticlePreview extends StatelessWidget { + const HorizontalArticlePreview({ + super.key, + required this.data, + this.minutes, + }); + + final ArticleData data; + final int? minutes; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText( + data.category, + style: textTheme.titleMedium, + ), + const SizedBox(height: 12), + SelectableText( + data.title, + style: textTheme.headlineSmall!.copyWith(fontSize: 16), + ), + ], + ), + ), + if (minutes != null) ...[ + SelectableText( + GalleryLocalizations.of(context)!.craneMinutes(minutes!), + style: textTheme.bodyLarge, + ), + const SizedBox(width: 8), + ], + FadeInImagePlaceholder( + image: AssetImage(data.imageUrl, package: 'flutter_gallery_assets'), + placeholder: Container( + color: Colors.black.withOpacity(0.1), + width: 64 / (1 / data.imageAspectRatio), + height: 64, + ), + fit: BoxFit.cover, + excludeFromSemantics: true, + ), + ], + ); + } +} + +class VerticalArticlePreview extends StatelessWidget { + const VerticalArticlePreview({ + super.key, + required this.data, + this.width, + this.headlineTextStyle, + this.showSnippet = false, + }); + + final ArticleData data; + final double? width; + final TextStyle? headlineTextStyle; + final bool showSnippet; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + + return SizedBox( + width: width ?? double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: double.infinity, + child: FadeInImagePlaceholder( + image: AssetImage( + data.imageUrl, + package: 'flutter_gallery_assets', + ), + placeholder: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { + return Container( + color: Colors.black.withOpacity(0.1), + width: constraints.maxWidth, + height: constraints.maxWidth / data.imageAspectRatio, + ); + }), + fit: BoxFit.fitWidth, + width: double.infinity, + excludeFromSemantics: true, + ), + ), + const SizedBox(height: 12), + SelectableText( + data.category, + style: textTheme.titleMedium, + ), + const SizedBox(height: 12), + SelectableText( + data.title, + style: headlineTextStyle ?? textTheme.headlineSmall, + ), + if (showSnippet) ...[ + const SizedBox(height: 4), + SelectableText( + data.snippet!, + style: textTheme.bodyMedium, + ), + ], + ], + ), + ); + } +} + +List buildArticlePreviewItems(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + final Widget articleDivider = Container( + margin: const EdgeInsets.symmetric(vertical: 16), + color: Colors.black.withOpacity(0.07), + height: 1, + ); + final Widget sectionDivider = Container( + margin: const EdgeInsets.symmetric(vertical: 16), + color: Colors.black.withOpacity(0.2), + height: 1, + ); + final TextTheme textTheme = Theme.of(context).textTheme; + + return [ + VerticalArticlePreview( + data: ArticleData( + imageUrl: 'fortnightly/fortnightly_healthcare.jpg', + imageAspectRatio: 391 / 248, + category: localizations.fortnightlyMenuWorld.toUpperCase(), + title: localizations.fortnightlyHeadlineHealthcare, + ), + headlineTextStyle: textTheme.headlineSmall!.copyWith(fontSize: 20), + ), + articleDivider, + HorizontalArticlePreview( + data: ArticleData( + imageUrl: 'fortnightly/fortnightly_war.png', + imageAspectRatio: 1, + category: localizations.fortnightlyMenuPolitics.toUpperCase(), + title: localizations.fortnightlyHeadlineWar, + ), + ), + articleDivider, + HorizontalArticlePreview( + data: ArticleData( + imageUrl: 'fortnightly/fortnightly_gas.png', + imageAspectRatio: 1, + category: localizations.fortnightlyMenuTech.toUpperCase(), + title: localizations.fortnightlyHeadlineGasoline, + ), + ), + sectionDivider, + SelectableText( + localizations.fortnightlyLatestUpdates, + style: textTheme.titleLarge, + ), + articleDivider, + HorizontalArticlePreview( + data: ArticleData( + imageUrl: 'fortnightly/fortnightly_army.png', + imageAspectRatio: 1, + category: localizations.fortnightlyMenuPolitics.toUpperCase(), + title: localizations.fortnightlyHeadlineArmy, + ), + minutes: 2, + ), + articleDivider, + HorizontalArticlePreview( + data: ArticleData( + imageUrl: 'fortnightly/fortnightly_stocks.png', + imageAspectRatio: 77 / 64, + category: localizations.fortnightlyMenuWorld.toUpperCase(), + title: localizations.fortnightlyHeadlineStocks, + ), + minutes: 5, + ), + articleDivider, + HorizontalArticlePreview( + data: ArticleData( + imageUrl: 'fortnightly/fortnightly_fabrics.png', + imageAspectRatio: 76 / 64, + category: localizations.fortnightlyMenuTech.toUpperCase(), + title: localizations.fortnightlyHeadlineFabrics, + ), + minutes: 4, + ), + articleDivider, + ]; +} + +class HashtagBar extends StatelessWidget { + const HashtagBar({super.key}); + + @override + Widget build(BuildContext context) { + final Container verticalDivider = Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + color: Colors.black.withOpacity(0.1), + width: 1, + ); + final TextTheme textTheme = Theme.of(context).textTheme; + final double height = 32 * reducedTextScale(context); + + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + return SizedBox( + height: height, + child: ListView( + restorationId: 'hashtag_bar_list_view', + scrollDirection: Axis.horizontal, + children: [ + const SizedBox(width: 16), + Center( + child: SelectableText( + '#${localizations.fortnightlyTrendingTechDesign}', + style: textTheme.titleSmall, + ), + ), + verticalDivider, + Center( + child: SelectableText( + '#${localizations.fortnightlyTrendingReform}', + style: textTheme.titleSmall, + ), + ), + verticalDivider, + Center( + child: SelectableText( + '#${localizations.fortnightlyTrendingHealthcareRevolution}', + style: textTheme.titleSmall, + ), + ), + verticalDivider, + Center( + child: SelectableText( + '#${localizations.fortnightlyTrendingGreenArmy}', + style: textTheme.titleSmall, + ), + ), + verticalDivider, + Center( + child: SelectableText( + '#${localizations.fortnightlyTrendingStocks}', + style: textTheme.titleSmall, + ), + ), + verticalDivider, + ], + ), + ); + } +} + +class NavigationMenu extends StatelessWidget { + const NavigationMenu({super.key, this.isCloseable = false}); + + final bool isCloseable; + + @override + Widget build(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return ListView( + children: [ + if (isCloseable) + Row( + children: [ + IconButton( + icon: const Icon(Icons.close), + tooltip: MaterialLocalizations.of(context).closeButtonTooltip, + onPressed: () => Navigator.pop(context), + ), + Image.asset( + 'fortnightly/fortnightly_title.png', + package: 'flutter_gallery_assets', + excludeFromSemantics: true, + ), + ], + ), + const SizedBox(height: 32), + MenuItem( + localizations.fortnightlyMenuFrontPage, + header: true, + ), + MenuItem(localizations.fortnightlyMenuWorld), + MenuItem(localizations.fortnightlyMenuUS), + MenuItem(localizations.fortnightlyMenuPolitics), + MenuItem(localizations.fortnightlyMenuBusiness), + MenuItem(localizations.fortnightlyMenuTech), + MenuItem(localizations.fortnightlyMenuScience), + MenuItem(localizations.fortnightlyMenuSports), + MenuItem(localizations.fortnightlyMenuTravel), + MenuItem(localizations.fortnightlyMenuCulture), + ], + ); + } +} + +class MenuItem extends StatelessWidget { + const MenuItem(this.title, {super.key, this.header = false}); + + final String title; + final bool header; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Container( + width: 32, + alignment: Alignment.centerLeft, + child: header ? null : const Icon(Icons.arrow_drop_down), + ), + Expanded( + child: SelectableText( + title, + style: Theme.of(context).textTheme.titleMedium!.copyWith( + fontWeight: header ? FontWeight.w700 : FontWeight.w600, + fontSize: 16, + ), + ), + ), + ], + ), + ); + } +} + +class StockItem extends StatelessWidget { + const StockItem({ + super.key, + required this.ticker, + required this.price, + required this.percent, + }); + + final String ticker; + final String price; + final double percent; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + final NumberFormat percentFormat = NumberFormat.decimalPercentPattern( + locale: GalleryOptions.of(context).locale.toString(), + decimalDigits: 2, + ); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText(ticker, style: textTheme.titleMedium), + const SizedBox(height: 2), + Row( + children: [ + Expanded( + child: SelectableText( + price, + style: textTheme.titleSmall!.copyWith( + color: textTheme.titleSmall!.color!.withOpacity(0.75), + ), + ), + ), + SelectableText( + percent > 0 ? '+' : '-', + style: textTheme.titleSmall!.copyWith( + fontSize: 12, + color: percent > 0 + ? const Color(0xff20CF63) + : const Color(0xff661FFF), + ), + ), + const SizedBox(width: 4), + SelectableText( + percentFormat.format(percent.abs() / 100), + style: textTheme.bodySmall!.copyWith( + fontSize: 12, + color: textTheme.titleSmall!.color!.withOpacity(0.75), + ), + ), + ], + ) + ], + ); + } +} + +List buildStockItems(BuildContext context) { + final Widget articleDivider = Container( + margin: const EdgeInsets.symmetric(vertical: 16), + color: Colors.black.withOpacity(0.07), + height: 1, + ); + const double imageAspectRatio = 165 / 55; + + return [ + SizedBox( + width: double.infinity, + child: FadeInImagePlaceholder( + image: const AssetImage( + 'fortnightly/fortnightly_chart.png', + package: 'flutter_gallery_assets', + ), + placeholder: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { + return Container( + color: Colors.black.withOpacity(0.1), + width: constraints.maxWidth, + height: constraints.maxWidth / imageAspectRatio, + ); + }), + width: double.infinity, + fit: BoxFit.contain, + excludeFromSemantics: true, + ), + ), + articleDivider, + const StockItem( + ticker: 'DIJA', + price: '7,031.21', + percent: -0.48, + ), + articleDivider, + const StockItem( + ticker: 'SP', + price: '1,967.84', + percent: -0.23, + ), + articleDivider, + const StockItem( + ticker: 'Nasdaq', + price: '6,211.46', + percent: 0.52, + ), + articleDivider, + const StockItem( + ticker: 'Nikkei', + price: '5,891', + percent: 1.16, + ), + articleDivider, + const StockItem( + ticker: 'DJ Total', + price: '89.02', + percent: 0.80, + ), + articleDivider, + ]; +} + +class VideoPreview extends StatelessWidget { + const VideoPreview({ + super.key, + required this.data, + required this.time, + }); + + final ArticleData data; + final String time; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: double.infinity, + child: FadeInImagePlaceholder( + image: AssetImage( + data.imageUrl, + package: 'flutter_gallery_assets', + ), + placeholder: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { + return Container( + color: Colors.black.withOpacity(0.1), + width: constraints.maxWidth, + height: constraints.maxWidth / data.imageAspectRatio, + ); + }), + fit: BoxFit.contain, + width: double.infinity, + excludeFromSemantics: true, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + Expanded( + child: SelectableText( + data.category, + style: textTheme.titleMedium, + ), + ), + SelectableText(time, style: textTheme.bodyLarge) + ], + ), + const SizedBox(height: 4), + SelectableText(data.title, + style: textTheme.headlineSmall!.copyWith(fontSize: 16)), + ], + ); + } +} + +List buildVideoPreviewItems(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return [ + VideoPreview( + data: ArticleData( + imageUrl: 'fortnightly/fortnightly_feminists.jpg', + imageAspectRatio: 148 / 88, + category: localizations.fortnightlyMenuPolitics.toUpperCase(), + title: localizations.fortnightlyHeadlineFeminists, + ), + time: '2:31', + ), + const SizedBox(height: 32), + VideoPreview( + data: ArticleData( + imageUrl: 'fortnightly/fortnightly_bees.jpg', + imageAspectRatio: 148 / 88, + category: localizations.fortnightlyMenuUS.toUpperCase(), + title: localizations.fortnightlyHeadlineBees, + ), + time: '1:37', + ), + ]; +} + +ThemeData buildTheme(BuildContext context) { + final TextTheme lightTextTheme = ThemeData.light().textTheme; + return ThemeData( + scaffoldBackgroundColor: Colors.white, + appBarTheme: AppBarTheme( + color: Colors.white, + elevation: 0, + iconTheme: IconTheme.of(context).copyWith(color: Colors.black), + ), + highlightColor: Colors.transparent, + textTheme: TextTheme( + // preview snippet + bodyMedium: GoogleFonts.merriweather( + fontWeight: FontWeight.w300, + fontSize: 16, + textStyle: lightTextTheme.bodyMedium, + ), + // time in latest updates + bodyLarge: GoogleFonts.libreFranklin( + fontWeight: FontWeight.w500, + fontSize: 11, + color: Colors.black.withOpacity(0.5), + textStyle: lightTextTheme.bodyLarge, + ), + // preview headlines + headlineSmall: GoogleFonts.libreFranklin( + fontWeight: FontWeight.w500, + fontSize: 16, + textStyle: lightTextTheme.headlineSmall, + ), + // (caption 2), preview category, stock ticker + titleMedium: GoogleFonts.robotoCondensed( + fontWeight: FontWeight.w700, + fontSize: 16, + ), + titleSmall: GoogleFonts.libreFranklin( + fontWeight: FontWeight.w400, + fontSize: 14, + textStyle: lightTextTheme.titleSmall, + ), + // section titles: Top Highlights, Last Updated... + titleLarge: GoogleFonts.merriweather( + fontWeight: FontWeight.w700, + fontStyle: FontStyle.italic, + fontSize: 14, + textStyle: lightTextTheme.titleLarge, + ), + ), + ); +} diff --git a/dev/integration_tests/new_gallery/lib/studies/rally/app.dart b/dev/integration_tests/new_gallery/lib/studies/rally/app.dart new file mode 100644 index 0000000000..0a6c02d894 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/rally/app.dart @@ -0,0 +1,114 @@ +// 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:flutter/services.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import '../../data/gallery_options.dart'; +import '../../gallery_localizations.dart'; +import '../../layout/letter_spacing.dart'; +import 'colors.dart'; +import 'home.dart'; +import 'login.dart'; +import 'routes.dart' as routes; + +/// The RallyApp is a MaterialApp with a theme and 2 routes. +/// +/// The home route is the main page with tabs for sub pages. +/// The login route is the initial route. +class RallyApp extends StatelessWidget { + const RallyApp({super.key}); + + static const String loginRoute = routes.loginRoute; + static const String homeRoute = routes.homeRoute; + + static const SharedAxisPageTransitionsBuilder sharedZAxisTransitionBuilder = SharedAxisPageTransitionsBuilder( + fillColor: RallyColors.primaryBackground, + transitionType: SharedAxisTransitionType.scaled, + ); + + @override + Widget build(BuildContext context) { + return MaterialApp( + restorationScopeId: 'rally_app', + title: 'Rally', + debugShowCheckedModeBanner: false, + theme: _buildRallyTheme().copyWith( + platform: GalleryOptions.of(context).platform, + pageTransitionsTheme: PageTransitionsTheme( + builders: { + for (final TargetPlatform type in TargetPlatform.values) + type: sharedZAxisTransitionBuilder, + }, + ), + ), + localizationsDelegates: GalleryLocalizations.localizationsDelegates, + supportedLocales: GalleryLocalizations.supportedLocales, + locale: GalleryOptions.of(context).locale, + initialRoute: loginRoute, + routes: { + homeRoute: (BuildContext context) => const HomePage(), + loginRoute: (BuildContext context) => const LoginPage(), + }, + ); + } + + ThemeData _buildRallyTheme() { + final ThemeData base = ThemeData.dark(); + return ThemeData( + appBarTheme: const AppBarTheme( + systemOverlayStyle: SystemUiOverlayStyle.light, + backgroundColor: RallyColors.primaryBackground, + elevation: 0, + ), + scaffoldBackgroundColor: RallyColors.primaryBackground, + focusColor: RallyColors.focusColor, + textTheme: _buildRallyTextTheme(base.textTheme), + inputDecorationTheme: const InputDecorationTheme( + labelStyle: TextStyle( + color: RallyColors.gray, + fontWeight: FontWeight.w500, + ), + filled: true, + fillColor: RallyColors.inputBackground, + focusedBorder: InputBorder.none, + ), + visualDensity: VisualDensity.standard, + colorScheme: base.colorScheme.copyWith( + primary: RallyColors.primaryBackground, + ), + ); + } + + TextTheme _buildRallyTextTheme(TextTheme base) { + return base + .copyWith( + bodyMedium: GoogleFonts.robotoCondensed( + fontSize: 14, + fontWeight: FontWeight.w400, + letterSpacing: letterSpacingOrNone(0.5), + ), + bodyLarge: GoogleFonts.eczar( + fontSize: 40, + fontWeight: FontWeight.w400, + letterSpacing: letterSpacingOrNone(1.4), + ), + labelLarge: GoogleFonts.robotoCondensed( + fontWeight: FontWeight.w700, + letterSpacing: letterSpacingOrNone(2.8), + ), + headlineSmall: GoogleFonts.eczar( + fontSize: 40, + fontWeight: FontWeight.w600, + letterSpacing: letterSpacingOrNone(1.4), + ), + ) + .apply( + displayColor: Colors.white, + bodyColor: Colors.white, + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/rally/charts/line_chart.dart b/dev/integration_tests/new_gallery/lib/studies/rally/charts/line_chart.dart new file mode 100644 index 0000000000..8386e6e947 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/rally/charts/line_chart.dart @@ -0,0 +1,299 @@ +// 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:flutter/material.dart'; +import 'package:flutter/semantics.dart'; +import 'package:intl/intl.dart' as intl; + +import '../../../data/gallery_options.dart'; +import '../../../layout/adaptive.dart'; +import '../../../layout/text_scale.dart'; +import '../colors.dart'; +import '../data.dart'; +import '../formatters.dart'; + +class RallyLineChart extends StatelessWidget { + const RallyLineChart({ + super.key, + this.events = const [], + }); + + final List events; + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: RallyLineChartPainter( + dateFormat: dateFormatMonthYear(context), + numberFormat: usdWithSignFormat(context), + events: events, + labelStyle: Theme.of(context).textTheme.bodyMedium!, + textDirection: GalleryOptions.of(context).resolvedTextDirection(), + textScaleFactor: reducedTextScale(context), + padding: isDisplayDesktop(context) + ? const EdgeInsets.symmetric(vertical: 22) + : EdgeInsets.zero, + ), + ); + } +} + +class RallyLineChartPainter extends CustomPainter { + RallyLineChartPainter({ + required this.dateFormat, + required this.numberFormat, + required this.events, + required this.labelStyle, + required this.textDirection, + required this.textScaleFactor, + required this.padding, + }); + + // The style for the labels. + final TextStyle labelStyle; + + // The text direction for the text. + final TextDirection? textDirection; + + // The text scale factor for the text. + final double textScaleFactor; + + // The padding around the text. + final EdgeInsets padding; + + // The format for the dates. + final intl.DateFormat dateFormat; + + // The currency format. + final intl.NumberFormat numberFormat; + + // Events to plot on the line as points. + final List events; + + // Number of days to plot. + // This is hardcoded to reflect the dummy data, but would be dynamic in a real + // app. + final int numDays = 52; + + // Beginning of window. The end is this plus numDays. + // This is hardcoded to reflect the dummy data, but would be dynamic in a real + // app. + final DateTime startDate = DateTime.utc(2018, 12); + + // Ranges uses to lerp the pixel points. + // This is hardcoded to reflect the dummy data, but would be dynamic in a real + // app. + final double maxAmount = 2000; // minAmount is assumed to be 0 + + // The number of milliseconds in a day. This is the inherit period fot the + // points in this line. + static const int millisInDay = 24 * 60 * 60 * 1000; + + // Amount to shift the tick drawing by so that the Sunday ticks do not start + // on the edge. + final int tickShift = 3; + + // Arbitrary unit of space for absolute positioned painting. + final double space = 16; + + @override + void paint(Canvas canvas, Size size) { + final double labelHeight = space + space * (textScaleFactor - 1); + final double ticksHeight = 3 * space; + final double ticksTop = size.height - labelHeight - ticksHeight - space; + final double labelsTop = size.height - labelHeight; + _drawLine( + canvas, + Rect.fromLTWH(0, 0, size.width, size.height - labelHeight - ticksHeight), + ); + _drawXAxisTicks( + canvas, + Rect.fromLTWH(0, ticksTop, size.width, ticksHeight), + ); + _drawXAxisLabels( + canvas, + Rect.fromLTWH(0, labelsTop, size.width, labelHeight), + ); + } + + // Since we're only using fixed dummy data, we can set this to false. In a + // real app we would have the data as part of the state and repaint when it's + // changed. + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; + + @override + SemanticsBuilderCallback get semanticsBuilder { + return (Size size) { + final List amounts = _amountsPerDay(numDays); + + // We divide the graph and the amounts into [numGroups] groups, with + // [numItemsPerGroup] amounts per group. + const int numGroups = 10; + final int numItemsPerGroup = amounts.length ~/ numGroups; + + // For each group we calculate the median value. + final List medians = List.generate( + numGroups, + (int i) { + final int middleIndex = i * numItemsPerGroup + numItemsPerGroup ~/ 2; + if (numItemsPerGroup.isEven) { + return (amounts[middleIndex] + amounts[middleIndex + 1]) / 2; + } else { + return amounts[middleIndex]; + } + }, + ); + + // Return a list of [CustomPainterSemantics] with the length of + // [numGroups], all have the same width with the median amount as label. + return List.generate(numGroups, (int i) { + return CustomPainterSemantics( + rect: Offset((i / numGroups) * size.width, 0) & + Size(size.width / numGroups, size.height), + properties: SemanticsProperties( + label: numberFormat.format(medians[i]), + textDirection: textDirection, + ), + ); + }); + }; + } + + /// Returns the amount of money in the account for the [numDays] given + /// from the [startDate]. + List _amountsPerDay(int numDays) { + // Arbitrary value for the first point. In a real app, a wider range of + // points would be used that go beyond the boundaries of the screen. + double lastAmount = 600.0; + + // Align the points with equal deltas (1 day) as a cumulative sum. + int startMillis = startDate.millisecondsSinceEpoch; + + final List amounts = []; + for (int i = 0; i < numDays; i++) { + final int endMillis = startMillis + millisInDay * 1; + final List filteredEvents = events.where( + (DetailedEventData e) { + return startMillis <= e.date.millisecondsSinceEpoch && + e.date.millisecondsSinceEpoch < endMillis; + }, + ).toList(); + lastAmount += sumOf(filteredEvents, (DetailedEventData e) => e.amount); + amounts.add(lastAmount); + startMillis = endMillis; + } + return amounts; + } + + void _drawLine(Canvas canvas, Rect rect) { + final Paint linePaint = Paint() + ..color = RallyColors.accountColor(2) + ..style = PaintingStyle.stroke + ..strokeWidth = 2; + + // Try changing this value between 1, 7, 15, etc. + const int smoothing = 1; + + final List amounts = _amountsPerDay(numDays + smoothing); + final List points = []; + for (int i = 0; i < amounts.length; i++) { + final double x = i / numDays * rect.width; + final double y = (maxAmount - amounts[i]) / maxAmount * rect.height; + points.add(Offset(x, y)); + } + + // Add last point of the graph to make sure we take up the full width. + points.add( + Offset( + rect.width, + (maxAmount - amounts[numDays - 1]) / maxAmount * rect.height, + ), + ); + + final Path path = Path(); + path.moveTo(points[0].dx, points[0].dy); + for (int i = 1; i < numDays - smoothing + 2; i += smoothing) { + final double x1 = points[i].dx; + final double y1 = points[i].dy; + final double x2 = (x1 + points[i + smoothing].dx) / 2; + final double y2 = (y1 + points[i + smoothing].dy) / 2; + path.quadraticBezierTo(x1, y1, x2, y2); + } + canvas.drawPath(path, linePaint); + } + + /// Draw the X-axis increment markers at constant width intervals. + void _drawXAxisTicks(Canvas canvas, Rect rect) { + for (int i = 0; i < numDays; i++) { + final double x = rect.width / numDays * i; + canvas.drawRect( + Rect.fromPoints( + Offset(x, i % 7 == tickShift ? rect.top : rect.center.dy), + Offset(x, rect.bottom), + ), + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1 + ..color = RallyColors.gray25, + ); + } + } + + /// Set X-axis labels under the X-axis increment markers. + void _drawXAxisLabels(Canvas canvas, Rect rect) { + final TextStyle selectedLabelStyle = labelStyle.copyWith( + fontWeight: FontWeight.w700, + fontSize: labelStyle.fontSize! * textScaleFactor, + ); + final TextStyle unselectedLabelStyle = labelStyle.copyWith( + fontWeight: FontWeight.w700, + color: RallyColors.gray25, + fontSize: labelStyle.fontSize! * textScaleFactor, + ); + + // We use toUpperCase to format the dates. This function uses the language + // independent Unicode mapping and thus only works in some languages. + final TextPainter leftLabel = TextPainter( + text: TextSpan( + text: dateFormat.format(startDate).toUpperCase(), + style: unselectedLabelStyle, + ), + textDirection: textDirection, + ); + leftLabel.layout(); + leftLabel.paint(canvas, + Offset(rect.left + space / 2 + padding.vertical, rect.topCenter.dy)); + + final TextPainter centerLabel = TextPainter( + text: TextSpan( + text: dateFormat + .format(DateTime(startDate.year, startDate.month + 1)) + .toUpperCase(), + style: selectedLabelStyle, + ), + textDirection: textDirection, + ); + centerLabel.layout(); + final double x = (rect.width - centerLabel.width) / 2; + final double y = rect.topCenter.dy; + centerLabel.paint(canvas, Offset(x, y)); + + final TextPainter rightLabel = TextPainter( + text: TextSpan( + text: dateFormat + .format(DateTime(startDate.year, startDate.month + 2)) + .toUpperCase(), + style: unselectedLabelStyle, + ), + textDirection: textDirection, + ); + rightLabel.layout(); + rightLabel.paint( + canvas, + Offset(rect.right - centerLabel.width - space / 2 - padding.vertical, + rect.topCenter.dy), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/rally/charts/pie_chart.dart b/dev/integration_tests/new_gallery/lib/studies/rally/charts/pie_chart.dart new file mode 100644 index 0000000000..1e3d896f6c --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/rally/charts/pie_chart.dart @@ -0,0 +1,292 @@ +// 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 'dart:math' as math; + +import 'package:flutter/material.dart'; + +import '../../../data/gallery_options.dart'; +import '../../../layout/letter_spacing.dart'; +import '../../../layout/text_scale.dart'; +import '../colors.dart'; +import '../data.dart'; +import '../formatters.dart'; + +/// A colored piece of the [RallyPieChart]. +class RallyPieChartSegment { + const RallyPieChartSegment({ + required this.color, + required this.value, + }); + + final Color color; + final double value; +} + +/// The max height and width of the [RallyPieChart]. +const double pieChartMaxSize = 500.0; + +List buildSegmentsFromAccountItems( + List items) { + return List.generate( + items.length, + (int i) { + return RallyPieChartSegment( + color: RallyColors.accountColor(i), + value: items[i].primaryAmount, + ); + }, + ); +} + +List buildSegmentsFromBillItems(List items) { + return List.generate( + items.length, + (int i) { + return RallyPieChartSegment( + color: RallyColors.billColor(i), + value: items[i].primaryAmount, + ); + }, + ); +} + +List buildSegmentsFromBudgetItems( + List items) { + return List.generate( + items.length, + (int i) { + return RallyPieChartSegment( + color: RallyColors.budgetColor(i), + value: items[i].primaryAmount - items[i].amountUsed, + ); + }, + ); +} + +/// An animated circular pie chart to represent pieces of a whole, which can +/// have empty space. +class RallyPieChart extends StatefulWidget { + const RallyPieChart({ + super.key, + required this.heroLabel, + required this.heroAmount, + required this.wholeAmount, + required this.segments, + }); + + final String heroLabel; + final double heroAmount; + final double wholeAmount; + final List segments; + + @override + State createState() => _RallyPieChartState(); +} + +class _RallyPieChartState extends State + with SingleTickerProviderStateMixin { + late AnimationController controller; + late Animation animation; + + @override + void initState() { + super.initState(); + controller = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + ); + animation = CurvedAnimation( + parent: TweenSequence(>[ + TweenSequenceItem( + tween: Tween(begin: 0, end: 0), + weight: 1, + ), + TweenSequenceItem( + tween: Tween(begin: 0, end: 1), + weight: 1.5, + ), + ]).animate(controller), + curve: Curves.decelerate); + controller.forward(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MergeSemantics( + child: _AnimatedRallyPieChart( + animation: animation, + centerLabel: widget.heroLabel, + centerAmount: widget.heroAmount, + total: widget.wholeAmount, + segments: widget.segments, + ), + ); + } +} + +class _AnimatedRallyPieChart extends AnimatedWidget { + const _AnimatedRallyPieChart({ + required this.animation, + required this.centerLabel, + required this.centerAmount, + required this.total, + required this.segments, + }) : super(listenable: animation); + + final Animation animation; + final String centerLabel; + final double centerAmount; + final double total; + final List segments; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + final TextStyle labelTextStyle = textTheme.bodyMedium!.copyWith( + fontSize: 14, + letterSpacing: letterSpacingOrNone(0.5), + ); + + return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { + // When the widget is larger, we increase the font size. + TextStyle? headlineStyle = constraints.maxHeight >= pieChartMaxSize + ? textTheme.headlineSmall!.copyWith(fontSize: 70) + : textTheme.headlineSmall; + + // With a large text scale factor, we set a max font size. + if (GalleryOptions.of(context).textScaleFactor(context) > 1.0) { + headlineStyle = headlineStyle!.copyWith( + fontSize: headlineStyle.fontSize! / reducedTextScale(context), + ); + } + + return DecoratedBox( + decoration: _RallyPieChartOutlineDecoration( + maxFraction: animation.value, + total: total, + segments: segments, + ), + child: Container( + height: constraints.maxHeight, + alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + centerLabel, + style: labelTextStyle, + ), + SelectableText( + usdWithSignFormat(context).format(centerAmount), + style: headlineStyle, + ), + ], + ), + ), + ); + }); + } +} + +class _RallyPieChartOutlineDecoration extends Decoration { + const _RallyPieChartOutlineDecoration({ + required this.maxFraction, + required this.total, + required this.segments, + }); + + final double maxFraction; + final double total; + final List segments; + + @override + BoxPainter createBoxPainter([VoidCallback? onChanged]) { + return _RallyPieChartOutlineBoxPainter( + maxFraction: maxFraction, + wholeAmount: total, + segments: segments, + ); + } +} + +class _RallyPieChartOutlineBoxPainter extends BoxPainter { + _RallyPieChartOutlineBoxPainter({ + required this.maxFraction, + required this.wholeAmount, + required this.segments, + }); + + final double maxFraction; + final double wholeAmount; + final List segments; + static const double wholeRadians = 2 * math.pi; + static const double spaceRadians = wholeRadians / 180; + + @override + void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { + // Create two padded reacts to draw arcs in: one for colored arcs and one for + // inner bg arc. + const double strokeWidth = 4.0; + final double outerRadius = math.min( + configuration.size!.width, + configuration.size!.height, + ) / + 2; + final Rect outerRect = Rect.fromCircle( + center: configuration.size!.center(offset), + radius: outerRadius - strokeWidth * 3, + ); + final Rect innerRect = Rect.fromCircle( + center: configuration.size!.center(offset), + radius: outerRadius - strokeWidth * 4, + ); + + // Paint each arc with spacing. + double cumulativeSpace = 0.0; + double cumulativeTotal = 0.0; + for (final RallyPieChartSegment segment in segments) { + final Paint paint = Paint()..color = segment.color; + final double startAngle = _calculateStartAngle(cumulativeTotal, cumulativeSpace); + final double sweepAngle = _calculateSweepAngle(segment.value, 0); + canvas.drawArc(outerRect, startAngle, sweepAngle, true, paint); + cumulativeTotal += segment.value; + cumulativeSpace += spaceRadians; + } + + // Paint any remaining space black (e.g. budget amount remaining). + final double remaining = wholeAmount - cumulativeTotal; + if (remaining > 0) { + final Paint paint = Paint()..color = Colors.black; + final double startAngle = + _calculateStartAngle(cumulativeTotal, spaceRadians * segments.length); + final double sweepAngle = _calculateSweepAngle(remaining, -spaceRadians); + canvas.drawArc(outerRect, startAngle, sweepAngle, true, paint); + } + + // Paint a smaller inner circle to cover the painted arcs, so they are + // display as segments. + final Paint bgPaint = Paint()..color = RallyColors.primaryBackground; + canvas.drawArc(innerRect, 0, 2 * math.pi, true, bgPaint); + } + + double _calculateAngle(double amount, double offset) { + final double wholeMinusSpacesRadians = + wholeRadians - (segments.length * spaceRadians); + return maxFraction * + (amount / wholeAmount * wholeMinusSpacesRadians + offset); + } + + double _calculateStartAngle(double total, double offset) => + _calculateAngle(total, offset) - math.pi / 2; + + double _calculateSweepAngle(double total, double offset) => + _calculateAngle(total, offset); +} diff --git a/dev/integration_tests/new_gallery/lib/studies/rally/charts/vertical_fraction_bar.dart b/dev/integration_tests/new_gallery/lib/studies/rally/charts/vertical_fraction_bar.dart new file mode 100644 index 0000000000..4fe85b8537 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/rally/charts/vertical_fraction_bar.dart @@ -0,0 +1,40 @@ +// 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:flutter/material.dart'; + +class VerticalFractionBar extends StatelessWidget { + const VerticalFractionBar({ + super.key, + this.color, + required this.fraction, + }); + + final Color? color; + final double fraction; + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { + return SizedBox( + height: constraints.maxHeight, + width: 4, + child: Column( + children: [ + SizedBox( + height: (1 - fraction) * constraints.maxHeight, + child: Container( + color: Colors.black, + ), + ), + SizedBox( + height: fraction * constraints.maxHeight, + child: Container(color: color), + ), + ], + ), + ); + }); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/rally/colors.dart b/dev/integration_tests/new_gallery/lib/studies/rally/colors.dart new file mode 100644 index 0000000000..a96aa51afe --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/rally/colors.dart @@ -0,0 +1,62 @@ +// 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 'dart:ui'; + +/// Most color assignments in Rally are not like the typical color +/// assignments that are common in other apps. Instead of primarily mapping to +/// component type and part, they are assigned round robin based on layout. +class RallyColors { + static const List accountColors = [ + Color(0xFF005D57), + Color(0xFF04B97F), + Color(0xFF37EFBA), + Color(0xFF007D51), + ]; + + static const List billColors = [ + Color(0xFFFFDC78), + Color(0xFFFF6951), + Color(0xFFFFD7D0), + Color(0xFFFFAC12), + ]; + + static const List budgetColors = [ + Color(0xFFB2F2FF), + Color(0xFFB15DFF), + Color(0xFF72DEFF), + Color(0xFF0082FB), + ]; + + static const Color gray = Color(0xFFD8D8D8); + static const Color gray60 = Color(0x99D8D8D8); + static const Color gray25 = Color(0x40D8D8D8); + static const Color white60 = Color(0x99FFFFFF); + static const Color primaryBackground = Color(0xFF33333D); + static const Color inputBackground = Color(0xFF26282F); + static const Color cardBackground = Color(0x03FEFEFE); + static const Color buttonColor = Color(0xFF09AF79); + static const Color focusColor = Color(0xCCFFFFFF); + static const Color dividerColor = Color(0xAA282828); + + /// Convenience method to get a single account color with position i. + static Color accountColor(int i) { + return cycledColor(accountColors, i); + } + + /// Convenience method to get a single bill color with position i. + static Color billColor(int i) { + return cycledColor(billColors, i); + } + + /// Convenience method to get a single budget color with position i. + static Color budgetColor(int i) { + return cycledColor(budgetColors, i); + } + + /// Gets a color from a list that is considered to be infinitely repeating. + static Color cycledColor(List colors, int i) { + return colors[i % colors.length]; + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/rally/data.dart b/dev/integration_tests/new_gallery/lib/studies/rally/data.dart new file mode 100644 index 0000000000..9aa675e134 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/rally/data.dart @@ -0,0 +1,382 @@ +// 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:flutter/material.dart'; + +import '../../gallery_localizations.dart'; +import 'formatters.dart'; + +/// Calculates the sum of the primary amounts of a list of [AccountData]. +double sumAccountDataPrimaryAmount(List items) => + sumOf(items, (AccountData item) => item.primaryAmount); + +/// Calculates the sum of the primary amounts of a list of [BillData]. +double sumBillDataPrimaryAmount(List items) => + sumOf(items, (BillData item) => item.primaryAmount); + +/// Calculates the sum of the primary amounts of a list of [BillData]. +double sumBillDataPaidAmount(List items) => sumOf( + items.where((BillData item) => item.isPaid).toList(), + (BillData item) => item.primaryAmount, + ); + +/// Calculates the sum of the primary amounts of a list of [BudgetData]. +double sumBudgetDataPrimaryAmount(List items) => + sumOf(items, (BudgetData item) => item.primaryAmount); + +/// Calculates the sum of the amounts used of a list of [BudgetData]. +double sumBudgetDataAmountUsed(List items) => + sumOf(items, (BudgetData item) => item.amountUsed); + +/// Utility function to sum up values in a list. +double sumOf(List list, double Function(T elt) getValue) { + double sum = 0.0; + for (final T elt in list) { + sum += getValue(elt); + } + return sum; +} + +/// A data model for an account. +/// +/// The [primaryAmount] is the balance of the account in USD. +class AccountData { + const AccountData({ + required this.name, + required this.primaryAmount, + required this.accountNumber, + }); + + /// The display name of this entity. + final String name; + + /// The primary amount or value of this entity. + final double primaryAmount; + + /// The full displayable account number. + final String accountNumber; +} + +/// A data model for a bill. +/// +/// The [primaryAmount] is the amount due in USD. +class BillData { + const BillData({ + required this.name, + required this.primaryAmount, + required this.dueDate, + this.isPaid = false, + }); + + /// The display name of this entity. + final String name; + + /// The primary amount or value of this entity. + final double primaryAmount; + + /// The due date of this bill. + final String dueDate; + + /// If this bill has been paid. + final bool isPaid; +} + +/// A data model for a budget. +/// +/// The [primaryAmount] is the budget cap in USD. +class BudgetData { + const BudgetData({ + required this.name, + required this.primaryAmount, + required this.amountUsed, + }); + + /// The display name of this entity. + final String name; + + /// The primary amount or value of this entity. + final double primaryAmount; + + /// Amount of the budget that is consumed or used. + final double amountUsed; +} + +/// A data model for an alert. +class AlertData { + AlertData({this.message, this.iconData}); + + /// The alert message to display. + final String? message; + + /// The icon to display with the alert. + final IconData? iconData; +} + +class DetailedEventData { + const DetailedEventData({ + required this.title, + required this.date, + required this.amount, + }); + + final String title; + final DateTime date; + final double amount; +} + +/// A data model for data displayed to the user. +class UserDetailData { + UserDetailData({ + required this.title, + required this.value, + }); + + /// The display name of this entity. + final String title; + + /// The value of this entity. + final String value; +} + +/// Class to return dummy data lists. +/// +/// In a real app, this might be replaced with some asynchronous service. +class DummyDataService { + static List getAccountDataList(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return [ + AccountData( + name: localizations.rallyAccountDataChecking, + primaryAmount: 2215.13, + accountNumber: '1234561234', + ), + AccountData( + name: localizations.rallyAccountDataHomeSavings, + primaryAmount: 8678.88, + accountNumber: '8888885678', + ), + AccountData( + name: localizations.rallyAccountDataCarSavings, + primaryAmount: 987.48, + accountNumber: '8888889012', + ), + AccountData( + name: localizations.rallyAccountDataVacation, + primaryAmount: 253, + accountNumber: '1231233456', + ), + ]; + } + + static List getAccountDetailList(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return [ + UserDetailData( + title: localizations.rallyAccountDetailDataAnnualPercentageYield, + value: percentFormat(context).format(0.001), + ), + UserDetailData( + title: localizations.rallyAccountDetailDataInterestRate, + value: usdWithSignFormat(context).format(1676.14), + ), + UserDetailData( + title: localizations.rallyAccountDetailDataInterestYtd, + value: usdWithSignFormat(context).format(81.45), + ), + UserDetailData( + title: localizations.rallyAccountDetailDataInterestPaidLastYear, + value: usdWithSignFormat(context).format(987.12), + ), + UserDetailData( + title: localizations.rallyAccountDetailDataNextStatement, + value: shortDateFormat(context).format(DateTime.utc(2019, 12, 25)), + ), + UserDetailData( + title: localizations.rallyAccountDetailDataAccountOwner, + value: 'Philip Cao', + ), + ]; + } + + static List getDetailedEventItems() { + // The following titles are not localized as they're product/brand names. + return [ + DetailedEventData( + title: 'Genoe', + date: DateTime.utc(2019, 1, 24), + amount: -16.54, + ), + DetailedEventData( + title: 'Fortnightly Subscribe', + date: DateTime.utc(2019, 1, 5), + amount: -12.54, + ), + DetailedEventData( + title: 'Circle Cash', + date: DateTime.utc(2019, 1, 5), + amount: 365.65, + ), + DetailedEventData( + title: 'Crane Hospitality', + date: DateTime.utc(2019, 1, 4), + amount: -705.13, + ), + DetailedEventData( + title: 'ABC Payroll', + date: DateTime.utc(2018, 12, 15), + amount: 1141.43, + ), + DetailedEventData( + title: 'Shrine', + date: DateTime.utc(2018, 12, 15), + amount: -88.88, + ), + DetailedEventData( + title: 'Foodmates', + date: DateTime.utc(2018, 12, 4), + amount: -11.69, + ), + ]; + } + + static List getBillDataList(BuildContext context) { + // The following names are not localized as they're product/brand names. + return [ + BillData( + name: 'RedPay Credit', + primaryAmount: 45.36, + dueDate: dateFormatAbbreviatedMonthDay(context) + .format(DateTime.utc(2019, 1, 29)), + ), + BillData( + name: 'Rent', + primaryAmount: 1200, + dueDate: dateFormatAbbreviatedMonthDay(context) + .format(DateTime.utc(2019, 2, 9)), + isPaid: true, + ), + BillData( + name: 'TabFine Credit', + primaryAmount: 87.33, + dueDate: dateFormatAbbreviatedMonthDay(context) + .format(DateTime.utc(2019, 2, 22)), + ), + BillData( + name: 'ABC Loans', + primaryAmount: 400, + dueDate: dateFormatAbbreviatedMonthDay(context) + .format(DateTime.utc(2019, 2, 29)), + ), + ]; + } + + static List getBillDetailList(BuildContext context, + {required double dueTotal, required double paidTotal}) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return [ + UserDetailData( + title: localizations.rallyBillDetailTotalAmount, + value: usdWithSignFormat(context).format(paidTotal + dueTotal), + ), + UserDetailData( + title: localizations.rallyBillDetailAmountPaid, + value: usdWithSignFormat(context).format(paidTotal), + ), + UserDetailData( + title: localizations.rallyBillDetailAmountDue, + value: usdWithSignFormat(context).format(dueTotal), + ), + ]; + } + + static List getBudgetDataList(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return [ + BudgetData( + name: localizations.rallyBudgetCategoryCoffeeShops, + primaryAmount: 70, + amountUsed: 45.49, + ), + BudgetData( + name: localizations.rallyBudgetCategoryGroceries, + primaryAmount: 170, + amountUsed: 16.45, + ), + BudgetData( + name: localizations.rallyBudgetCategoryRestaurants, + primaryAmount: 170, + amountUsed: 123.25, + ), + BudgetData( + name: localizations.rallyBudgetCategoryClothing, + primaryAmount: 70, + amountUsed: 19.45, + ), + ]; + } + + static List getBudgetDetailList(BuildContext context, + {required double capTotal, required double usedTotal}) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return [ + UserDetailData( + title: localizations.rallyBudgetDetailTotalCap, + value: usdWithSignFormat(context).format(capTotal), + ), + UserDetailData( + title: localizations.rallyBudgetDetailAmountUsed, + value: usdWithSignFormat(context).format(usedTotal), + ), + UserDetailData( + title: localizations.rallyBudgetDetailAmountLeft, + value: usdWithSignFormat(context).format(capTotal - usedTotal), + ), + ]; + } + + static List getSettingsTitles(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return [ + localizations.rallySettingsManageAccounts, + localizations.rallySettingsTaxDocuments, + localizations.rallySettingsPasscodeAndTouchId, + localizations.rallySettingsNotifications, + localizations.rallySettingsPersonalInformation, + localizations.rallySettingsPaperlessSettings, + localizations.rallySettingsFindAtms, + localizations.rallySettingsHelp, + localizations.rallySettingsSignOut, + ]; + } + + static List getAlerts(BuildContext context) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return [ + AlertData( + message: localizations.rallyAlertsMessageHeadsUpShopping( + percentFormat(context, decimalDigits: 0).format(0.9)), + iconData: Icons.sort, + ), + AlertData( + message: localizations.rallyAlertsMessageSpentOnRestaurants( + usdWithSignFormat(context, decimalDigits: 0).format(120)), + iconData: Icons.sort, + ), + AlertData( + message: localizations.rallyAlertsMessageATMFees( + usdWithSignFormat(context, decimalDigits: 0).format(24)), + iconData: Icons.credit_card, + ), + AlertData( + message: localizations.rallyAlertsMessageCheckingAccount( + percentFormat(context, decimalDigits: 0).format(0.04)), + iconData: Icons.attach_money, + ), + AlertData( + message: localizations.rallyAlertsMessageUnassignedTransactions(16), + iconData: Icons.not_interested, + ), + ]; + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/rally/finance.dart b/dev/integration_tests/new_gallery/lib/studies/rally/finance.dart new file mode 100644 index 0000000000..669bf71cd8 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/rally/finance.dart @@ -0,0 +1,483 @@ +// 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 'dart:math' as math; + +import 'package:animations/animations.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import '../../data/gallery_options.dart'; +import '../../gallery_localizations.dart'; +import '../../layout/adaptive.dart'; +import '../../layout/text_scale.dart'; +import 'charts/line_chart.dart'; +import 'charts/pie_chart.dart'; +import 'charts/vertical_fraction_bar.dart'; +import 'colors.dart'; +import 'data.dart'; +import 'formatters.dart'; + +class FinancialEntityView extends StatelessWidget { + const FinancialEntityView({ + super.key, + required this.heroLabel, + required this.heroAmount, + required this.wholeAmount, + required this.segments, + required this.financialEntityCards, + }) : assert(segments.length == financialEntityCards.length); + + /// The amounts to assign each item. + final List segments; + final String heroLabel; + final double heroAmount; + final double wholeAmount; + final List financialEntityCards; + + @override + Widget build(BuildContext context) { + final double maxWidth = pieChartMaxSize + (cappedTextScale(context) - 1.0) * 100.0; + return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { + return Column( + children: [ + ConstrainedBox( + constraints: BoxConstraints( + // We decrease the max height to ensure the [RallyPieChart] does + // not take up the full height when it is smaller than + // [kPieChartMaxSize]. + maxHeight: math.min( + constraints.biggest.shortestSide * 0.9, + maxWidth, + ), + ), + child: RallyPieChart( + heroLabel: heroLabel, + heroAmount: heroAmount, + wholeAmount: wholeAmount, + segments: segments, + ), + ), + const SizedBox(height: 24), + Container( + height: 1, + constraints: BoxConstraints(maxWidth: maxWidth), + color: RallyColors.inputBackground, + ), + Container( + constraints: BoxConstraints(maxWidth: maxWidth), + color: RallyColors.cardBackground, + child: Column( + children: financialEntityCards, + ), + ), + ], + ); + }); + } +} + +/// A reusable widget to show balance information of a single entity as a card. +class FinancialEntityCategoryView extends StatelessWidget { + const FinancialEntityCategoryView({ + super.key, + required this.indicatorColor, + required this.indicatorFraction, + required this.title, + required this.subtitle, + required this.semanticsLabel, + required this.amount, + required this.suffix, + }); + + final Color indicatorColor; + final double indicatorFraction; + final String title; + final String subtitle; + final String semanticsLabel; + final String amount; + final Widget suffix; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + return Semantics.fromProperties( + properties: SemanticsProperties( + button: true, + enabled: true, + label: semanticsLabel, + ), + excludeSemantics: true, + // TODO(x): State restoration of FinancialEntityCategoryDetailsPage on mobile is blocked because OpenContainer does not support restorablePush, https://github.com/flutter/gallery/issues/570. + child: OpenContainer( + transitionDuration: const Duration(milliseconds: 350), + openBuilder: (BuildContext context, void Function() openContainer) => + FinancialEntityCategoryDetailsPage(), + openColor: RallyColors.primaryBackground, + closedColor: RallyColors.primaryBackground, + closedElevation: 0, + closedBuilder: (BuildContext context, void Function() openContainer) { + return TextButton( + style: TextButton.styleFrom(foregroundColor: Colors.black), + onPressed: openContainer, + child: Column( + children: [ + Container( + padding: + const EdgeInsets.symmetric(vertical: 16, horizontal: 8), + child: Row( + children: [ + Container( + alignment: Alignment.center, + height: 32 + 60 * (cappedTextScale(context) - 1), + padding: const EdgeInsets.symmetric(horizontal: 12), + child: VerticalFractionBar( + color: indicatorColor, + fraction: indicatorFraction, + ), + ), + Expanded( + child: Wrap( + alignment: WrapAlignment.spaceBetween, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: textTheme.bodyMedium! + .copyWith(fontSize: 16), + ), + Text( + subtitle, + style: textTheme.bodyMedium! + .copyWith(color: RallyColors.gray60), + ), + ], + ), + Text( + amount, + style: textTheme.bodyLarge!.copyWith( + fontSize: 20, + color: RallyColors.gray, + ), + ), + ], + ), + ), + Container( + constraints: const BoxConstraints(minWidth: 32), + padding: const EdgeInsetsDirectional.only(start: 12), + child: suffix, + ), + ], + ), + ), + const Divider( + height: 1, + indent: 16, + endIndent: 16, + color: RallyColors.dividerColor, + ), + ], + ), + ); + }, + ), + ); + } +} + +/// Data model for [FinancialEntityCategoryView]. +class FinancialEntityCategoryModel { + const FinancialEntityCategoryModel( + this.indicatorColor, + this.indicatorFraction, + this.title, + this.subtitle, + this.usdAmount, + this.suffix, + ); + + final Color indicatorColor; + final double indicatorFraction; + final String title; + final String subtitle; + final double usdAmount; + final Widget suffix; +} + +FinancialEntityCategoryView buildFinancialEntityFromAccountData( + AccountData model, + int accountDataIndex, + BuildContext context, +) { + final String amount = usdWithSignFormat(context).format(model.primaryAmount); + final String shortAccountNumber = model.accountNumber.substring(6); + return FinancialEntityCategoryView( + suffix: const Icon(Icons.chevron_right, color: Colors.grey), + title: model.name, + subtitle: '• • • • • • $shortAccountNumber', + semanticsLabel: GalleryLocalizations.of(context)!.rallyAccountAmount( + model.name, + shortAccountNumber, + amount, + ), + indicatorColor: RallyColors.accountColor(accountDataIndex), + indicatorFraction: 1, + amount: amount, + ); +} + +FinancialEntityCategoryView buildFinancialEntityFromBillData( + BillData model, + int billDataIndex, + BuildContext context, +) { + final String amount = usdWithSignFormat(context).format(model.primaryAmount); + return FinancialEntityCategoryView( + suffix: const Icon(Icons.chevron_right, color: Colors.grey), + title: model.name, + subtitle: model.dueDate, + semanticsLabel: GalleryLocalizations.of(context)!.rallyBillAmount( + model.name, + model.dueDate, + amount, + ), + indicatorColor: RallyColors.billColor(billDataIndex), + indicatorFraction: 1, + amount: amount, + ); +} + +FinancialEntityCategoryView buildFinancialEntityFromBudgetData( + BudgetData model, + int budgetDataIndex, + BuildContext context, +) { + final String amountUsed = usdWithSignFormat(context).format(model.amountUsed); + final String primaryAmount = usdWithSignFormat(context).format(model.primaryAmount); + final String amount = + usdWithSignFormat(context).format(model.primaryAmount - model.amountUsed); + + return FinancialEntityCategoryView( + suffix: Text( + GalleryLocalizations.of(context)!.rallyFinanceLeft, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: RallyColors.gray60, fontSize: 10), + ), + title: model.name, + subtitle: '$amountUsed / $primaryAmount', + semanticsLabel: GalleryLocalizations.of(context)!.rallyBudgetAmount( + model.name, + model.amountUsed, + model.primaryAmount, + amount, + ), + indicatorColor: RallyColors.budgetColor(budgetDataIndex), + indicatorFraction: model.amountUsed / model.primaryAmount, + amount: amount, + ); +} + +List buildAccountDataListViews( + List items, + BuildContext context, +) { + return List.generate( + items.length, + (int i) => buildFinancialEntityFromAccountData(items[i], i, context), + ); +} + +List buildBillDataListViews( + List items, + BuildContext context, +) { + return List.generate( + items.length, + (int i) => buildFinancialEntityFromBillData(items[i], i, context), + ); +} + +List buildBudgetDataListViews( + List items, + BuildContext context, +) { + return [ + for (int i = 0; i < items.length; i++) + buildFinancialEntityFromBudgetData(items[i], i, context) + ]; +} + +class FinancialEntityCategoryDetailsPage extends StatelessWidget { + FinancialEntityCategoryDetailsPage({super.key}); + + final List items = + DummyDataService.getDetailedEventItems(); + + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + + return ApplyTextOptions( + child: Scaffold( + appBar: AppBar( + elevation: 0, + centerTitle: true, + title: Text( + GalleryLocalizations.of(context)!.rallyAccountDataChecking, + style: + Theme.of(context).textTheme.bodyMedium!.copyWith(fontSize: 18), + ), + ), + body: Column( + children: [ + SizedBox( + height: 200, + width: double.infinity, + child: RallyLineChart(events: items), + ), + Expanded( + child: Padding( + padding: isDesktop ? const EdgeInsets.all(40) : EdgeInsets.zero, + child: ListView( + shrinkWrap: true, + children: [ + for (final DetailedEventData detailedEventData in items) + _DetailedEventCard( + title: detailedEventData.title, + date: detailedEventData.date, + amount: detailedEventData.amount, + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} + +class _DetailedEventCard extends StatelessWidget { + const _DetailedEventCard({ + required this.title, + required this.date, + required this.amount, + }); + + final String title; + final DateTime date; + final double amount; + + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + return TextButton( + style: TextButton.styleFrom( + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(horizontal: 16), + ), + onPressed: () {}, + child: Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 16), + width: double.infinity, + child: isDesktop + ? Row( + children: [ + Expanded( + child: _EventTitle(title: title), + ), + _EventDate(date: date), + Expanded( + child: Align( + alignment: AlignmentDirectional.centerEnd, + child: _EventAmount(amount: amount), + ), + ), + ], + ) + : Wrap( + alignment: WrapAlignment.spaceBetween, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _EventTitle(title: title), + _EventDate(date: date), + ], + ), + _EventAmount(amount: amount), + ], + ), + ), + SizedBox( + height: 1, + child: Container( + color: RallyColors.dividerColor, + ), + ), + ], + ), + ); + } +} + +class _EventAmount extends StatelessWidget { + const _EventAmount({required this.amount}); + + final double amount; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + return Text( + usdWithSignFormat(context).format(amount), + style: textTheme.bodyLarge!.copyWith( + fontSize: 20, + color: RallyColors.gray, + ), + ); + } +} + +class _EventDate extends StatelessWidget { + const _EventDate({required this.date}); + + final DateTime date; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + return Text( + shortDateFormat(context).format(date), + semanticsLabel: longDateFormat(context).format(date), + style: textTheme.bodyMedium!.copyWith(color: RallyColors.gray60), + ); + } +} + +class _EventTitle extends StatelessWidget { + const _EventTitle({required this.title}); + + final String title; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + return Text( + title, + style: textTheme.bodyMedium!.copyWith(fontSize: 16), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/rally/formatters.dart b/dev/integration_tests/new_gallery/lib/studies/rally/formatters.dart new file mode 100644 index 0000000000..0e5a066d64 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/rally/formatters.dart @@ -0,0 +1,45 @@ +// 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:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import '../../data/gallery_options.dart'; + +/// Get the locale string for the context. +String locale(BuildContext context) => + GalleryOptions.of(context).locale.toString(); + +/// Currency formatter for USD. +NumberFormat usdWithSignFormat(BuildContext context, {int decimalDigits = 2}) { + return NumberFormat.currency( + locale: locale(context), + name: r'$', + decimalDigits: decimalDigits, + ); +} + +/// Percent formatter with two decimal points. +NumberFormat percentFormat(BuildContext context, {int decimalDigits = 2}) { + return NumberFormat.decimalPercentPattern( + locale: locale(context), + decimalDigits: decimalDigits, + ); +} + +/// Date formatter with year / number month / day. +DateFormat shortDateFormat(BuildContext context) => + DateFormat.yMd(locale(context)); + +/// Date formatter with year / month / day. +DateFormat longDateFormat(BuildContext context) => + DateFormat.yMMMMd(locale(context)); + +/// Date formatter with abbreviated month and day. +DateFormat dateFormatAbbreviatedMonthDay(BuildContext context) => + DateFormat.MMMd(locale(context)); + +/// Date formatter with year and abbreviated month. +DateFormat dateFormatMonthYear(BuildContext context) => + DateFormat.yMMM(locale(context)); diff --git a/dev/integration_tests/new_gallery/lib/studies/rally/home.dart b/dev/integration_tests/new_gallery/lib/studies/rally/home.dart new file mode 100644 index 0000000000..ee93310028 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/rally/home.dart @@ -0,0 +1,383 @@ +// 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:flutter/material.dart'; + +import '../../data/gallery_options.dart'; +import '../../gallery_localizations.dart'; +import '../../layout/adaptive.dart'; +import '../../layout/text_scale.dart'; +import 'tabs/accounts.dart'; +import 'tabs/bills.dart'; +import 'tabs/budgets.dart'; +import 'tabs/overview.dart'; +import 'tabs/settings.dart'; + +const int tabCount = 5; +const int turnsToRotateRight = 1; +const int turnsToRotateLeft = 3; + +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State + with SingleTickerProviderStateMixin, RestorationMixin { + late TabController _tabController; + RestorableInt tabIndex = RestorableInt(0); + + @override + String get restorationId => 'home_page'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(tabIndex, 'tab_index'); + _tabController.index = tabIndex.value; + } + + @override + void initState() { + super.initState(); + _tabController = TabController(length: tabCount, vsync: this) + ..addListener(() { + // Set state to make sure that the [_RallyTab] widgets get updated when changing tabs. + setState(() { + tabIndex.value = _tabController.index; + }); + }); + } + + @override + void dispose() { + _tabController.dispose(); + tabIndex.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final bool isDesktop = isDisplayDesktop(context); + Widget tabBarView; + if (isDesktop) { + final bool isTextDirectionRtl = + GalleryOptions.of(context).resolvedTextDirection() == + TextDirection.rtl; + final int verticalRotation = + isTextDirectionRtl ? turnsToRotateLeft : turnsToRotateRight; + final int revertVerticalRotation = + isTextDirectionRtl ? turnsToRotateRight : turnsToRotateLeft; + tabBarView = Row( + children: [ + Container( + width: 150 + 50 * (cappedTextScale(context) - 1), + alignment: Alignment.topCenter, + padding: const EdgeInsets.symmetric(vertical: 32), + child: Column( + children: [ + const SizedBox(height: 24), + ExcludeSemantics( + child: SizedBox( + height: 80, + child: Image.asset( + 'logo.png', + package: 'rally_assets', + ), + ), + ), + const SizedBox(height: 24), + // Rotate the tab bar, so the animation is vertical for desktops. + RotatedBox( + quarterTurns: verticalRotation, + child: _RallyTabBar( + tabs: _buildTabs( + context: context, theme: theme, isVertical: true) + .map( + (Widget widget) { + // Revert the rotation on the tabs. + return RotatedBox( + quarterTurns: revertVerticalRotation, + child: widget, + ); + }, + ).toList(), + tabController: _tabController, + ), + ), + ], + ), + ), + Expanded( + // Rotate the tab views so we can swipe up and down. + child: RotatedBox( + quarterTurns: verticalRotation, + child: TabBarView( + controller: _tabController, + children: _buildTabViews().map( + (Widget widget) { + // Revert the rotation on the tab views. + return RotatedBox( + quarterTurns: revertVerticalRotation, + child: widget, + ); + }, + ).toList(), + ), + ), + ), + ], + ); + } else { + tabBarView = Column( + children: [ + _RallyTabBar( + tabs: _buildTabs(context: context, theme: theme), + tabController: _tabController, + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: _buildTabViews(), + ), + ), + ], + ); + } + return ApplyTextOptions( + child: Scaffold( + body: SafeArea( + // For desktop layout we do not want to have SafeArea at the top and + // bottom to display 100% height content on the accounts view. + top: !isDesktop, + bottom: !isDesktop, + child: Theme( + // This theme effectively removes the default visual touch + // feedback for tapping a tab, which is replaced with a custom + // animation. + data: theme.copyWith( + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + child: FocusTraversalGroup( + policy: OrderedTraversalPolicy(), + child: tabBarView, + ), + ), + ), + ), + ); + } + + List _buildTabs( + {required BuildContext context, + required ThemeData theme, + bool isVertical = false}) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return [ + _RallyTab( + theme: theme, + iconData: Icons.pie_chart, + title: localizations.rallyTitleOverview, + tabIndex: 0, + tabController: _tabController, + isVertical: isVertical, + ), + _RallyTab( + theme: theme, + iconData: Icons.attach_money, + title: localizations.rallyTitleAccounts, + tabIndex: 1, + tabController: _tabController, + isVertical: isVertical, + ), + _RallyTab( + theme: theme, + iconData: Icons.money_off, + title: localizations.rallyTitleBills, + tabIndex: 2, + tabController: _tabController, + isVertical: isVertical, + ), + _RallyTab( + theme: theme, + iconData: Icons.table_chart, + title: localizations.rallyTitleBudgets, + tabIndex: 3, + tabController: _tabController, + isVertical: isVertical, + ), + _RallyTab( + theme: theme, + iconData: Icons.settings, + title: localizations.rallyTitleSettings, + tabIndex: 4, + tabController: _tabController, + isVertical: isVertical, + ), + ]; + } + + List _buildTabViews() { + return const [ + OverviewView(), + AccountsView(), + BillsView(), + BudgetsView(), + SettingsView(), + ]; + } +} + +class _RallyTabBar extends StatelessWidget { + const _RallyTabBar({ + required this.tabs, + this.tabController, + }); + + final List tabs; + final TabController? tabController; + + @override + Widget build(BuildContext context) { + return FocusTraversalOrder( + order: const NumericFocusOrder(0), + child: TabBar( + // Setting isScrollable to true prevents the tabs from being + // wrapped in [Expanded] widgets, which allows for more + // flexible sizes and size animations among tabs. + isScrollable: true, + labelPadding: EdgeInsets.zero, + tabs: tabs, + controller: tabController, + // This hides the tab indicator. + indicatorColor: Colors.transparent, + ), + ); + } +} + +class _RallyTab extends StatefulWidget { + _RallyTab({ + required ThemeData theme, + IconData? iconData, + required String title, + int? tabIndex, + required TabController tabController, + required this.isVertical, + }) : titleText = Text(title, style: theme.textTheme.labelLarge), + isExpanded = tabController.index == tabIndex, + icon = Icon(iconData, semanticLabel: title); + + final Text titleText; + final Icon icon; + final bool isExpanded; + final bool isVertical; + + @override + _RallyTabState createState() => _RallyTabState(); +} + +class _RallyTabState extends State<_RallyTab> + with SingleTickerProviderStateMixin { + late Animation _titleSizeAnimation; + late Animation _titleFadeAnimation; + late Animation _iconFadeAnimation; + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + _titleSizeAnimation = _controller.view; + _titleFadeAnimation = _controller.drive(CurveTween(curve: Curves.easeOut)); + _iconFadeAnimation = _controller.drive(Tween(begin: 0.6, end: 1)); + if (widget.isExpanded) { + _controller.value = 1; + } + } + + @override + void didUpdateWidget(_RallyTab oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isExpanded) { + _controller.forward(); + } else { + _controller.reverse(); + } + } + + @override + Widget build(BuildContext context) { + if (widget.isVertical) { + return Column( + children: [ + const SizedBox(height: 18), + FadeTransition( + opacity: _iconFadeAnimation, + child: widget.icon, + ), + const SizedBox(height: 12), + FadeTransition( + opacity: _titleFadeAnimation, + child: SizeTransition( + axisAlignment: -1, + sizeFactor: _titleSizeAnimation, + child: Center(child: ExcludeSemantics(child: widget.titleText)), + ), + ), + const SizedBox(height: 18), + ], + ); + } + + // Calculate the width of each unexpanded tab by counting the number of + // units and dividing it into the screen width. Each unexpanded tab is 1 + // unit, and there is always 1 expanded tab which is 1 unit + any extra + // space determined by the multiplier. + final double width = MediaQuery.of(context).size.width; + const int expandedTitleWidthMultiplier = 2; + final double unitWidth = width / (tabCount + expandedTitleWidthMultiplier); + + return ConstrainedBox( + constraints: const BoxConstraints(minHeight: 56), + child: Row( + children: [ + FadeTransition( + opacity: _iconFadeAnimation, + child: SizedBox( + width: unitWidth, + child: widget.icon, + ), + ), + FadeTransition( + opacity: _titleFadeAnimation, + child: SizeTransition( + axis: Axis.horizontal, + axisAlignment: -1, + sizeFactor: _titleSizeAnimation, + child: SizedBox( + width: unitWidth * expandedTitleWidthMultiplier, + child: Center( + child: ExcludeSemantics(child: widget.titleText), + ), + ), + ), + ), + ], + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/rally/login.dart b/dev/integration_tests/new_gallery/lib/studies/rally/login.dart new file mode 100644 index 0000000000..3c6e6482c8 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/rally/login.dart @@ -0,0 +1,420 @@ +// 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:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../../data/gallery_options.dart'; +import '../../gallery_localizations.dart'; +import '../../layout/adaptive.dart'; +import '../../layout/image_placeholder.dart'; +import '../../layout/text_scale.dart'; +import 'app.dart'; +import 'colors.dart'; + +class LoginPage extends StatefulWidget { + const LoginPage({super.key}); + + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State with RestorationMixin { + final RestorableTextEditingController _usernameController = + RestorableTextEditingController(); + final RestorableTextEditingController _passwordController = + RestorableTextEditingController(); + + @override + String get restorationId => 'login_page'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_usernameController, restorationId); + registerForRestoration(_passwordController, restorationId); + } + + @override + Widget build(BuildContext context) { + return ApplyTextOptions( + child: Scaffold( + body: SafeArea( + child: _MainView( + usernameController: _usernameController.value, + passwordController: _passwordController.value, + ), + ), + ), + ); + } + + @override + void dispose() { + _usernameController.dispose(); + _passwordController.dispose(); + super.dispose(); + } +} + +class _MainView extends StatelessWidget { + const _MainView({ + this.usernameController, + this.passwordController, + }); + + final TextEditingController? usernameController; + final TextEditingController? passwordController; + + void _login(BuildContext context) { + Navigator.of(context).restorablePushNamed(RallyApp.homeRoute); + } + + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + List listViewChildren; + + if (isDesktop) { + final double desktopMaxWidth = 400.0 + 100.0 * (cappedTextScale(context) - 1); + listViewChildren = [ + _UsernameInput( + maxWidth: desktopMaxWidth, + usernameController: usernameController, + ), + const SizedBox(height: 12), + _PasswordInput( + maxWidth: desktopMaxWidth, + passwordController: passwordController, + ), + _LoginButton( + maxWidth: desktopMaxWidth, + onTap: () { + _login(context); + }, + ), + ]; + } else { + listViewChildren = [ + const _SmallLogo(), + _UsernameInput( + usernameController: usernameController, + ), + const SizedBox(height: 12), + _PasswordInput( + passwordController: passwordController, + ), + _ThumbButton( + onTap: () { + _login(context); + }, + ), + ]; + } + + return Column( + children: [ + if (isDesktop) const _TopBar(), + Expanded( + child: Align( + alignment: isDesktop ? Alignment.center : Alignment.topCenter, + child: ListView( + restorationId: 'login_list_view', + shrinkWrap: true, + padding: const EdgeInsets.symmetric(horizontal: 24), + children: listViewChildren, + ), + ), + ), + ], + ); + } +} + +class _TopBar extends StatelessWidget { + const _TopBar(); + + @override + Widget build(BuildContext context) { + const SizedBox spacing = SizedBox(width: 30); + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return Container( + width: double.infinity, + margin: const EdgeInsets.only(top: 8), + padding: const EdgeInsets.symmetric(horizontal: 30), + child: Wrap( + alignment: WrapAlignment.spaceBetween, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + ExcludeSemantics( + child: SizedBox( + height: 80, + child: FadeInImagePlaceholder( + image: + const AssetImage('logo.png', package: 'rally_assets'), + placeholder: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { + return SizedBox( + width: constraints.maxHeight, + height: constraints.maxHeight, + ); + }), + ), + ), + ), + spacing, + Text( + localizations.rallyLoginLoginToRally, + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontSize: 35 / reducedTextScale(context), + fontWeight: FontWeight.w600, + ), + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + localizations.rallyLoginNoAccount, + style: Theme.of(context).textTheme.titleMedium, + ), + spacing, + _BorderButton( + text: localizations.rallyLoginSignUp, + ), + ], + ), + ], + ), + ); + } +} + +class _SmallLogo extends StatelessWidget { + const _SmallLogo(); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 64), + child: SizedBox( + height: 160, + child: ExcludeSemantics( + child: FadeInImagePlaceholder( + image: AssetImage('logo.png', package: 'rally_assets'), + placeholder: SizedBox.shrink(), + ), + ), + ), + ); + } +} + +class _UsernameInput extends StatelessWidget { + const _UsernameInput({ + this.maxWidth, + this.usernameController, + }); + + final double? maxWidth; + final TextEditingController? usernameController; + + @override + Widget build(BuildContext context) { + return Align( + child: Container( + constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), + child: TextField( + autofillHints: const [AutofillHints.username], + textInputAction: TextInputAction.next, + controller: usernameController, + decoration: InputDecoration( + labelText: GalleryLocalizations.of(context)!.rallyLoginUsername, + ), + ), + ), + ); + } +} + +class _PasswordInput extends StatelessWidget { + const _PasswordInput({ + this.maxWidth, + this.passwordController, + }); + + final double? maxWidth; + final TextEditingController? passwordController; + + @override + Widget build(BuildContext context) { + return Align( + child: Container( + constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), + child: TextField( + controller: passwordController, + decoration: InputDecoration( + labelText: GalleryLocalizations.of(context)!.rallyLoginPassword, + ), + obscureText: true, + ), + ), + ); + } +} + +class _ThumbButton extends StatefulWidget { + const _ThumbButton({ + required this.onTap, + }); + + final VoidCallback onTap; + + @override + _ThumbButtonState createState() => _ThumbButtonState(); +} + +class _ThumbButtonState extends State<_ThumbButton> { + BoxDecoration? borderDecoration; + + @override + Widget build(BuildContext context) { + return Semantics( + button: true, + enabled: true, + label: GalleryLocalizations.of(context)!.rallyLoginLabelLogin, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: widget.onTap, + child: Focus( + onKeyEvent: (FocusNode node, KeyEvent event) { + if (event is KeyDownEvent || event is KeyRepeatEvent) { + if (event.logicalKey == LogicalKeyboardKey.enter || + event.logicalKey == LogicalKeyboardKey.space) { + widget.onTap(); + return KeyEventResult.handled; + } + } + return KeyEventResult.ignored; + }, + onFocusChange: (bool hasFocus) { + if (hasFocus) { + setState(() { + borderDecoration = BoxDecoration( + border: Border.all( + color: Colors.white.withOpacity(0.5), + width: 2, + ), + ); + }); + } else { + setState(() { + borderDecoration = null; + }); + } + }, + child: Container( + decoration: borderDecoration, + height: 120, + child: ExcludeSemantics( + child: Image.asset( + 'thumb.png', + package: 'rally_assets', + ), + ), + ), + ), + ), + ), + ); + } +} + +class _LoginButton extends StatelessWidget { + const _LoginButton({ + required this.onTap, + this.maxWidth, + }); + + final double? maxWidth; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Align( + child: Container( + constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), + padding: const EdgeInsets.symmetric(vertical: 30), + child: Row( + children: [ + const Icon(Icons.check_circle_outline, + color: RallyColors.buttonColor), + const SizedBox(width: 12), + Text(GalleryLocalizations.of(context)!.rallyLoginRememberMe), + const Expanded(child: SizedBox.shrink()), + _FilledButton( + text: GalleryLocalizations.of(context)!.rallyLoginButtonLogin, + onTap: onTap, + ), + ], + ), + ), + ); + } +} + +class _BorderButton extends StatelessWidget { + const _BorderButton({required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: Colors.white, + side: const BorderSide(color: RallyColors.buttonColor), + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 24), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: () { + Navigator.of(context).restorablePushNamed(RallyApp.homeRoute); + }, + child: Text(text), + ); + } +} + +class _FilledButton extends StatelessWidget { + const _FilledButton({required this.text, required this.onTap}); + + final String text; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return TextButton( + style: TextButton.styleFrom( + foregroundColor: Colors.black, + backgroundColor: RallyColors.buttonColor, + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 24), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: onTap, + child: Row( + children: [ + const Icon(Icons.lock), + const SizedBox(width: 6), + Text(text), + ], + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/rally/routes.dart b/dev/integration_tests/new_gallery/lib/studies/rally/routes.dart new file mode 100644 index 0000000000..b49cf807ab --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/rally/routes.dart @@ -0,0 +1,6 @@ +// 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. + +const String loginRoute = '/rally/login'; +const String homeRoute = '/rally'; diff --git a/dev/integration_tests/new_gallery/lib/studies/rally/tabs/accounts.dart b/dev/integration_tests/new_gallery/lib/studies/rally/tabs/accounts.dart new file mode 100644 index 0000000000..c7d6a04aff --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/rally/tabs/accounts.dart @@ -0,0 +1,38 @@ +// 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:flutter/material.dart'; + +import '../../../gallery_localizations.dart'; +import '../charts/pie_chart.dart'; +import '../data.dart'; +import '../finance.dart'; +import 'sidebar.dart'; + +/// A page that shows a summary of accounts. +class AccountsView extends StatelessWidget { + const AccountsView({super.key}); + + @override + Widget build(BuildContext context) { + final List items = DummyDataService.getAccountDataList(context); + final List detailItems = DummyDataService.getAccountDetailList(context); + final double balanceTotal = sumAccountDataPrimaryAmount(items); + + return TabWithSidebar( + restorationId: 'accounts_view', + mainView: FinancialEntityView( + heroLabel: GalleryLocalizations.of(context)!.rallyAccountTotal, + heroAmount: balanceTotal, + segments: buildSegmentsFromAccountItems(items), + wholeAmount: balanceTotal, + financialEntityCards: buildAccountDataListViews(items, context), + ), + sidebarItems: [ + for (final UserDetailData item in detailItems) + SidebarItem(title: item.title, value: item.value) + ], + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/rally/tabs/bills.dart b/dev/integration_tests/new_gallery/lib/studies/rally/tabs/bills.dart new file mode 100644 index 0000000000..b2e205b719 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/rally/tabs/bills.dart @@ -0,0 +1,49 @@ +// 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:flutter/widgets.dart'; + +import '../../../gallery_localizations.dart'; +import '../charts/pie_chart.dart'; +import '../data.dart'; +import '../finance.dart'; +import 'sidebar.dart'; + +/// A page that shows a summary of bills. +class BillsView extends StatefulWidget { + const BillsView({super.key}); + + @override + State createState() => _BillsViewState(); +} + +class _BillsViewState extends State + with SingleTickerProviderStateMixin { + @override + Widget build(BuildContext context) { + final List items = DummyDataService.getBillDataList(context); + final double dueTotal = sumBillDataPrimaryAmount(items); + final double paidTotal = sumBillDataPaidAmount(items); + final List detailItems = DummyDataService.getBillDetailList( + context, + dueTotal: dueTotal, + paidTotal: paidTotal, + ); + + return TabWithSidebar( + restorationId: 'bills_view', + mainView: FinancialEntityView( + heroLabel: GalleryLocalizations.of(context)!.rallyBillsDue, + heroAmount: dueTotal, + segments: buildSegmentsFromBillItems(items), + wholeAmount: dueTotal, + financialEntityCards: buildBillDataListViews(items, context), + ), + sidebarItems: [ + for (final UserDetailData item in detailItems) + SidebarItem(title: item.title, value: item.value) + ], + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/rally/tabs/budgets.dart b/dev/integration_tests/new_gallery/lib/studies/rally/tabs/budgets.dart new file mode 100644 index 0000000000..fef1586016 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/rally/tabs/budgets.dart @@ -0,0 +1,48 @@ +// 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:flutter/widgets.dart'; + +import '../../../gallery_localizations.dart'; +import '../charts/pie_chart.dart'; +import '../data.dart'; +import '../finance.dart'; +import 'sidebar.dart'; + +class BudgetsView extends StatefulWidget { + const BudgetsView({super.key}); + + @override + State createState() => _BudgetsViewState(); +} + +class _BudgetsViewState extends State + with SingleTickerProviderStateMixin { + @override + Widget build(BuildContext context) { + final List items = DummyDataService.getBudgetDataList(context); + final double capTotal = sumBudgetDataPrimaryAmount(items); + final double usedTotal = sumBudgetDataAmountUsed(items); + final List detailItems = DummyDataService.getBudgetDetailList( + context, + capTotal: capTotal, + usedTotal: usedTotal, + ); + + return TabWithSidebar( + restorationId: 'budgets_view', + mainView: FinancialEntityView( + heroLabel: GalleryLocalizations.of(context)!.rallyBudgetLeft, + heroAmount: capTotal - usedTotal, + segments: buildSegmentsFromBudgetItems(items), + wholeAmount: capTotal, + financialEntityCards: buildBudgetDataListViews(items, context), + ), + sidebarItems: [ + for (final UserDetailData item in detailItems) + SidebarItem(title: item.title, value: item.value) + ], + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/rally/tabs/overview.dart b/dev/integration_tests/new_gallery/lib/studies/rally/tabs/overview.dart new file mode 100644 index 0000000000..f82aa5015b --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/rally/tabs/overview.dart @@ -0,0 +1,295 @@ +// 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 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import '../../../data/gallery_options.dart'; +import '../../../gallery_localizations.dart'; +import '../../../layout/adaptive.dart'; +import '../../../layout/text_scale.dart'; +import '../colors.dart'; +import '../data.dart'; +import '../finance.dart'; +import '../formatters.dart'; + +/// A page that shows a status overview. +class OverviewView extends StatefulWidget { + const OverviewView({super.key}); + + @override + State createState() => _OverviewViewState(); +} + +class _OverviewViewState extends State { + @override + Widget build(BuildContext context) { + final List alerts = DummyDataService.getAlerts(context); + + if (isDisplayDesktop(context)) { + const String sortKeyName = 'Overview'; + return SingleChildScrollView( + restorationId: 'overview_scroll_view', + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 24), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + flex: 7, + child: Semantics( + sortKey: const OrdinalSortKey(1, name: sortKeyName), + child: const _OverviewGrid(spacing: 24), + ), + ), + const SizedBox(width: 24), + Flexible( + flex: 3, + child: SizedBox( + width: 400, + child: Semantics( + sortKey: const OrdinalSortKey(2, name: sortKeyName), + child: FocusTraversalGroup( + child: _AlertsView(alerts: alerts), + ), + ), + ), + ), + ], + ), + ), + ); + } else { + return SingleChildScrollView( + restorationId: 'overview_scroll_view', + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Column( + children: [ + _AlertsView(alerts: alerts.sublist(0, 1)), + const SizedBox(height: 12), + const _OverviewGrid(spacing: 12), + ], + ), + ), + ); + } + } +} + +class _OverviewGrid extends StatelessWidget { + const _OverviewGrid({required this.spacing}); + + final double spacing; + + @override + Widget build(BuildContext context) { + final List accountDataList = DummyDataService.getAccountDataList(context); + final List billDataList = DummyDataService.getBillDataList(context); + final List budgetDataList = DummyDataService.getBudgetDataList(context); + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { + final double textScaleFactor = + GalleryOptions.of(context).textScaleFactor(context); + + // Only display multiple columns when the constraints allow it and we + // have a regular text scale factor. + const int minWidthForTwoColumns = 600; + final bool hasMultipleColumns = isDisplayDesktop(context) && + constraints.maxWidth > minWidthForTwoColumns && + textScaleFactor <= 2; + final double boxWidth = hasMultipleColumns + ? constraints.maxWidth / 2 - spacing / 2 + : double.infinity; + + return Wrap( + runSpacing: spacing, + children: [ + SizedBox( + width: boxWidth, + child: _FinancialView( + title: localizations.rallyAccounts, + total: sumAccountDataPrimaryAmount(accountDataList), + financialItemViews: + buildAccountDataListViews(accountDataList, context), + buttonSemanticsLabel: localizations.rallySeeAllAccounts, + order: 1, + ), + ), + if (hasMultipleColumns) SizedBox(width: spacing), + SizedBox( + width: boxWidth, + child: _FinancialView( + title: localizations.rallyBills, + total: sumBillDataPrimaryAmount(billDataList), + financialItemViews: buildBillDataListViews(billDataList, context), + buttonSemanticsLabel: localizations.rallySeeAllBills, + order: 2, + ), + ), + _FinancialView( + title: localizations.rallyBudgets, + total: sumBudgetDataPrimaryAmount(budgetDataList), + financialItemViews: + buildBudgetDataListViews(budgetDataList, context), + buttonSemanticsLabel: localizations.rallySeeAllBudgets, + order: 3, + ), + ], + ); + }); + } +} + +class _AlertsView extends StatelessWidget { + const _AlertsView({this.alerts}); + + final List? alerts; + + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + return Container( + padding: const EdgeInsetsDirectional.only(start: 16, top: 4, bottom: 4), + color: RallyColors.cardBackground, + child: Column( + children: [ + Container( + width: double.infinity, + padding: + isDesktop ? const EdgeInsets.symmetric(vertical: 16) : null, + child: MergeSemantics( + child: Wrap( + alignment: WrapAlignment.spaceBetween, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text(localizations.rallyAlerts), + if (!isDesktop) + TextButton( + style: TextButton.styleFrom( + foregroundColor: Colors.white, + ), + onPressed: () {}, + child: Text(localizations.rallySeeAll), + ), + ], + ), + ), + ), + for (final AlertData alert in alerts!) ...[ + Container(color: RallyColors.primaryBackground, height: 1), + _Alert(alert: alert), + ] + ], + ), + ); + } +} + +class _Alert extends StatelessWidget { + const _Alert({required this.alert}); + + final AlertData alert; + + @override + Widget build(BuildContext context) { + return MergeSemantics( + child: Container( + padding: isDisplayDesktop(context) + ? const EdgeInsets.symmetric(vertical: 8) + : null, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: SelectableText(alert.message!), + ), + SizedBox( + width: 100, + child: Align( + alignment: Alignment.topRight, + child: IconButton( + onPressed: () {}, + icon: Icon(alert.iconData, color: RallyColors.white60), + ), + ), + ), + ], + ), + ), + ); + } +} + +class _FinancialView extends StatelessWidget { + const _FinancialView({ + this.title, + this.total, + this.financialItemViews, + this.buttonSemanticsLabel, + this.order, + }); + + final String? title; + final String? buttonSemanticsLabel; + final double? total; + final List? financialItemViews; + final double? order; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + return FocusTraversalOrder( + order: NumericFocusOrder(order!), + child: ColoredBox( + color: RallyColors.cardBackground, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + MergeSemantics( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.only( + top: 16, + left: 16, + right: 16, + ), + child: SelectableText(title!), + ), + Padding( + padding: const EdgeInsets.only(left: 16, right: 16), + child: SelectableText( + usdWithSignFormat(context).format(total), + style: theme.textTheme.bodyLarge!.copyWith( + fontSize: 44 / reducedTextScale(context), + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + ...financialItemViews! + .sublist(0, math.min(financialItemViews!.length, 3)), + TextButton( + style: TextButton.styleFrom(foregroundColor: Colors.white), + onPressed: () {}, + child: Text( + GalleryLocalizations.of(context)!.rallySeeAll, + semanticsLabel: buttonSemanticsLabel, + ), + ), + ], + ), + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/rally/tabs/settings.dart b/dev/integration_tests/new_gallery/lib/studies/rally/tabs/settings.dart new file mode 100644 index 0000000000..b9db0de1b6 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/rally/tabs/settings.dart @@ -0,0 +1,65 @@ +// 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:flutter/material.dart'; +import '../../../layout/adaptive.dart'; +import '../colors.dart'; +import '../data.dart'; +import '../routes.dart' as rally_route; + +class SettingsView extends StatefulWidget { + const SettingsView({super.key}); + + @override + State createState() => _SettingsViewState(); +} + +class _SettingsViewState extends State { + @override + Widget build(BuildContext context) { + return FocusTraversalGroup( + child: Container( + padding: EdgeInsets.only(top: isDisplayDesktop(context) ? 24 : 0), + child: ListView( + restorationId: 'settings_list_view', + shrinkWrap: true, + children: [ + for (final String title + in DummyDataService.getSettingsTitles(context)) ...[ + _SettingsItem(title), + const Divider( + color: RallyColors.dividerColor, + height: 1, + ) + ] + ], + ), + ), + ); + } +} + +class _SettingsItem extends StatelessWidget { + const _SettingsItem(this.title); + + final String title; + + @override + Widget build(BuildContext context) { + return TextButton( + style: TextButton.styleFrom( + foregroundColor: Colors.white, + padding: EdgeInsets.zero, + ), + onPressed: () { + Navigator.of(context).restorablePushNamed(rally_route.loginRoute); + }, + child: Container( + alignment: AlignmentDirectional.centerStart, + padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 28), + child: Text(title), + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/rally/tabs/sidebar.dart b/dev/integration_tests/new_gallery/lib/studies/rally/tabs/sidebar.dart new file mode 100644 index 0000000000..467fc44a74 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/rally/tabs/sidebar.dart @@ -0,0 +1,95 @@ +// 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:flutter/material.dart'; +import '../../../layout/adaptive.dart'; +import '../colors.dart'; + +class TabWithSidebar extends StatelessWidget { + const TabWithSidebar({ + super.key, + this.restorationId, + required this.mainView, + required this.sidebarItems, + }); + + final Widget mainView; + final List sidebarItems; + final String? restorationId; + + @override + Widget build(BuildContext context) { + if (isDisplayDesktop(context)) { + return Row( + children: [ + Flexible( + flex: 2, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 24), + child: mainView, + ), + ), + ), + Expanded( + child: Container( + color: RallyColors.inputBackground, + padding: const EdgeInsetsDirectional.only(start: 24), + height: double.infinity, + alignment: AlignmentDirectional.centerStart, + child: ListView( + shrinkWrap: true, + children: sidebarItems, + ), + ), + ), + ], + ); + } else { + return SingleChildScrollView( + restorationId: restorationId, + child: mainView, + ); + } + } +} + +class SidebarItem extends StatelessWidget { + const SidebarItem({ + super.key, + required this.value, + required this.title, + }); + + final String value; + final String title; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8), + SelectableText( + title, + style: textTheme.bodyMedium!.copyWith( + fontSize: 16, + color: RallyColors.gray60, + ), + ), + const SizedBox(height: 8), + SelectableText( + value, + style: textTheme.bodyLarge!.copyWith(fontSize: 20), + ), + const SizedBox(height: 8), + Container( + color: RallyColors.primaryBackground, + height: 1, + ), + ], + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/reply/adaptive_nav.dart b/dev/integration_tests/new_gallery/lib/studies/reply/adaptive_nav.dart new file mode 100644 index 0000000000..357d9279c5 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/reply/adaptive_nav.dart @@ -0,0 +1,1280 @@ +// 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 'dart:math' as math; +import 'dart:ui' as ui; + +import 'package:animations/animations.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:provider/provider.dart'; + +import '../../data/gallery_options.dart'; +import '../../gallery_localizations.dart'; +import '../../layout/adaptive.dart'; +import 'app.dart'; +import 'bottom_drawer.dart'; +import 'colors.dart'; +import 'compose_page.dart'; +import 'mailbox_body.dart'; +import 'model/email_model.dart'; +import 'model/email_store.dart'; +import 'profile_avatar.dart'; +import 'search_page.dart'; +import 'waterfall_notched_rectangle.dart'; + +const String _assetsPackage = 'flutter_gallery_assets'; +const String _iconAssetLocation = 'reply/icons'; +const String _folderIconAssetLocation = '$_iconAssetLocation/twotone_folder.png'; +final GlobalKey desktopMailNavKey = GlobalKey(); +final GlobalKey mobileMailNavKey = GlobalKey(); +const double _kFlingVelocity = 2.0; +const Duration _kAnimationDuration = Duration(milliseconds: 300); + +class AdaptiveNav extends StatefulWidget { + const AdaptiveNav({super.key}); + + @override + State createState() => _AdaptiveNavState(); +} + +class _AdaptiveNavState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + final bool isTablet = isDisplaySmallDesktop(context); + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + final List<_Destination> navigationDestinations = <_Destination>[ + _Destination( + type: MailboxPageType.inbox, + textLabel: localizations.replyInboxLabel, + icon: '$_iconAssetLocation/twotone_inbox.png', + ), + _Destination( + type: MailboxPageType.starred, + textLabel: localizations.replyStarredLabel, + icon: '$_iconAssetLocation/twotone_star.png', + ), + _Destination( + type: MailboxPageType.sent, + textLabel: localizations.replySentLabel, + icon: '$_iconAssetLocation/twotone_send.png', + ), + _Destination( + type: MailboxPageType.trash, + textLabel: localizations.replyTrashLabel, + icon: '$_iconAssetLocation/twotone_delete.png', + ), + _Destination( + type: MailboxPageType.spam, + textLabel: localizations.replySpamLabel, + icon: '$_iconAssetLocation/twotone_error.png', + ), + _Destination( + type: MailboxPageType.drafts, + textLabel: localizations.replyDraftsLabel, + icon: '$_iconAssetLocation/twotone_drafts.png', + ), + ]; + + final Map folders = { + 'Receipts': _folderIconAssetLocation, + 'Pine Elementary': _folderIconAssetLocation, + 'Taxes': _folderIconAssetLocation, + 'Vacation': _folderIconAssetLocation, + 'Mortgage': _folderIconAssetLocation, + 'Freelance': _folderIconAssetLocation, + }; + + if (isDesktop) { + return _DesktopNav( + extended: !isTablet, + destinations: navigationDestinations, + folders: folders, + onItemTapped: _onDestinationSelected, + ); + } else { + return _MobileNav( + destinations: navigationDestinations, + folders: folders, + onItemTapped: _onDestinationSelected, + ); + } + } + + void _onDestinationSelected(int index, MailboxPageType destination) { + final EmailStore emailStore = Provider.of( + context, + listen: false, + ); + + final bool isDesktop = isDisplayDesktop(context); + + emailStore.selectedMailboxPage = destination; + + if (isDesktop) { + while (desktopMailNavKey.currentState!.canPop()) { + desktopMailNavKey.currentState!.pop(); + } + } + + if (emailStore.onMailView) { + if (!isDesktop) { + mobileMailNavKey.currentState!.pop(); + } + + emailStore.selectedEmailId = -1; + } + } +} + +class _DesktopNav extends StatefulWidget { + const _DesktopNav({ + required this.extended, + required this.destinations, + required this.folders, + required this.onItemTapped, + }); + + final bool extended; + final List<_Destination> destinations; + final Map folders; + final void Function(int, MailboxPageType) onItemTapped; + + @override + _DesktopNavState createState() => _DesktopNavState(); +} + +class _DesktopNavState extends State<_DesktopNav> + with SingleTickerProviderStateMixin { + late ValueNotifier _isExtended; + + @override + void initState() { + super.initState(); + _isExtended = ValueNotifier(widget.extended); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Row( + children: [ + Consumer( + builder: (BuildContext context, EmailStore model, Widget? child) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + final int selectedIndex = + widget.destinations.indexWhere((_Destination destination) { + return destination.type == model.selectedMailboxPage; + }); + return Container( + color: + Theme.of(context).navigationRailTheme.backgroundColor, + child: SingleChildScrollView( + clipBehavior: Clip.antiAlias, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: ValueListenableBuilder( + valueListenable: _isExtended, + builder: (BuildContext context, bool value, Widget? child) { + return NavigationRail( + destinations: [ + for (final _Destination destination in widget.destinations) + NavigationRailDestination( + icon: Material( + key: ValueKey( + 'Reply-${destination.textLabel}', + ), + color: Colors.transparent, + child: ImageIcon( + AssetImage( + destination.icon, + package: _assetsPackage, + ), + ), + ), + label: Text(destination.textLabel), + ), + ], + extended: _isExtended.value, + labelType: NavigationRailLabelType.none, + leading: _NavigationRailHeader( + extended: _isExtended, + ), + trailing: _NavigationRailFolderSection( + folders: widget.folders, + ), + selectedIndex: selectedIndex, + onDestinationSelected: (int index) { + widget.onItemTapped( + index, + widget.destinations[index].type, + ); + }, + ); + }, + ), + ), + ), + ), + ); + }, + ); + }, + ), + const VerticalDivider(thickness: 1, width: 1), + Expanded( + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 1340), + child: const _SharedAxisTransitionSwitcher( + defaultChild: _MailNavigator( + child: MailboxBody(), + ), + ), + ), + ), + ), + ], + ), + ); + } +} + +class _NavigationRailHeader extends StatelessWidget { + const _NavigationRailHeader({required this.extended}); + + final ValueNotifier extended; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + final Animation animation = NavigationRail.extendedAnimation(context); + + return AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget? child) { + return Align( + alignment: AlignmentDirectional.centerStart, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 56, + child: Row( + children: [ + const SizedBox(width: 6), + InkWell( + key: const ValueKey('ReplyLogo'), + borderRadius: const BorderRadius.all(Radius.circular(16)), + onTap: () { + extended.value = !extended.value; + }, + child: Row( + children: [ + Transform.rotate( + angle: animation.value * math.pi, + child: const Icon( + Icons.arrow_left, + color: ReplyColors.white50, + size: 16, + ), + ), + const _ReplyLogo(), + const SizedBox(width: 10), + Align( + alignment: AlignmentDirectional.centerStart, + widthFactor: animation.value, + child: Opacity( + opacity: animation.value, + child: Text( + 'REPLY', + style: textTheme.bodyLarge!.copyWith( + color: ReplyColors.white50, + ), + ), + ), + ), + SizedBox(width: 18 * animation.value), + ], + ), + ), + if (animation.value > 0) + Opacity( + opacity: animation.value, + child: const Row( + children: [ + SizedBox(width: 18), + ProfileAvatar( + avatar: 'reply/avatars/avatar_2.jpg', + radius: 16, + ), + SizedBox(width: 12), + Icon( + Icons.settings, + color: ReplyColors.white50, + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsetsDirectional.only( + start: 8, + ), + child: _ReplyFab(extended: extended.value), + ), + const SizedBox(height: 8), + ], + ), + ); + }, + ); + } +} + +class _NavigationRailFolderSection extends StatelessWidget { + const _NavigationRailFolderSection({required this.folders}); + + final Map folders; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final TextTheme textTheme = theme.textTheme; + final NavigationRailThemeData navigationRailTheme = theme.navigationRailTheme; + final Animation animation = NavigationRail.extendedAnimation(context); + + return AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget? child) { + return Visibility( + maintainAnimation: true, + maintainState: true, + visible: animation.value > 0, + child: Opacity( + opacity: animation.value, + child: Align( + widthFactor: animation.value, + alignment: AlignmentDirectional.centerStart, + child: SizedBox( + height: 485, + width: 256, + child: ListView( + padding: const EdgeInsets.all(12), + physics: const NeverScrollableScrollPhysics(), + children: [ + const Divider( + color: ReplyColors.blue200, + thickness: 0.4, + indent: 14, + endIndent: 16, + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsetsDirectional.only( + start: 16, + ), + child: Text( + 'FOLDERS', + style: textTheme.bodySmall!.copyWith( + color: navigationRailTheme + .unselectedLabelTextStyle!.color, + ), + ), + ), + const SizedBox(height: 8), + for (final String folder in folders.keys) + InkWell( + borderRadius: const BorderRadius.all( + Radius.circular(36), + ), + onTap: () {}, + child: Column( + children: [ + Row( + children: [ + const SizedBox(width: 12), + ImageIcon( + AssetImage( + folders[folder]!, + package: _assetsPackage, + ), + color: navigationRailTheme + .unselectedLabelTextStyle!.color, + ), + const SizedBox(width: 24), + Text( + folder, + style: textTheme.bodyLarge!.copyWith( + color: navigationRailTheme + .unselectedLabelTextStyle!.color, + ), + ), + const SizedBox(height: 72), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } +} + +class _MobileNav extends StatefulWidget { + const _MobileNav({ + required this.destinations, + required this.folders, + required this.onItemTapped, + }); + + final List<_Destination> destinations; + final Map folders; + final void Function(int, MailboxPageType) onItemTapped; + + @override + _MobileNavState createState() => _MobileNavState(); +} + +class _MobileNavState extends State<_MobileNav> with TickerProviderStateMixin { + final GlobalKey> _bottomDrawerKey = GlobalKey(debugLabel: 'Bottom Drawer'); + late AnimationController _drawerController; + late AnimationController _dropArrowController; + late AnimationController _bottomAppBarController; + late Animation _drawerCurve; + late Animation _dropArrowCurve; + late Animation _bottomAppBarCurve; + + @override + void initState() { + super.initState(); + _drawerController = AnimationController( + duration: _kAnimationDuration, + value: 0, + vsync: this, + )..addListener(() { + if (_drawerController.value < 0.01) { + setState(() { + //Reload state when drawer is at its smallest to toggle visibility + //If state is reloaded before this drawer closes abruptly instead + //of animating. + }); + } + }); + + _dropArrowController = AnimationController( + duration: _kAnimationDuration, + vsync: this, + ); + + _bottomAppBarController = AnimationController( + vsync: this, + value: 1, + duration: const Duration(milliseconds: 250), + ); + + _drawerCurve = CurvedAnimation( + parent: _drawerController, + curve: Easing.legacy, + reverseCurve: Easing.legacy.flipped, + ); + + _dropArrowCurve = CurvedAnimation( + parent: _dropArrowController, + curve: Easing.legacy, + reverseCurve: Easing.legacy.flipped, + ); + + _bottomAppBarCurve = CurvedAnimation( + parent: _bottomAppBarController, + curve: Easing.legacy, + reverseCurve: Easing.legacy.flipped, + ); + } + + @override + void dispose() { + _drawerController.dispose(); + _dropArrowController.dispose(); + _bottomAppBarController.dispose(); + super.dispose(); + } + + bool get _bottomDrawerVisible { + final AnimationStatus status = _drawerController.status; + return status == AnimationStatus.completed || + status == AnimationStatus.forward; + } + + void _toggleBottomDrawerVisibility() { + if (_drawerController.value < 0.4) { + _drawerController.animateTo(0.4, curve: Easing.legacy); + _dropArrowController.animateTo(0.35, curve: Easing.legacy); + return; + } + + _dropArrowController.forward(); + _drawerController.fling( + velocity: _bottomDrawerVisible ? -_kFlingVelocity : _kFlingVelocity, + ); + } + + double get _bottomDrawerHeight { + final RenderBox renderBox = + _bottomDrawerKey.currentContext!.findRenderObject()! as RenderBox; + return renderBox.size.height; + } + + void _handleDragUpdate(DragUpdateDetails details) { + _drawerController.value -= details.primaryDelta! / _bottomDrawerHeight; + } + + void _handleDragEnd(DragEndDetails details) { + if (_drawerController.isAnimating || + _drawerController.status == AnimationStatus.completed) { + return; + } + + final double flingVelocity = + details.velocity.pixelsPerSecond.dy / _bottomDrawerHeight; + + if (flingVelocity < 0.0) { + _drawerController.fling( + velocity: math.max(_kFlingVelocity, -flingVelocity), + ); + } else if (flingVelocity > 0.0) { + _dropArrowController.forward(); + _drawerController.fling( + velocity: math.min(-_kFlingVelocity, -flingVelocity), + ); + } else { + if (_drawerController.value < 0.6) { + _dropArrowController.forward(); + } + _drawerController.fling( + velocity: + _drawerController.value < 0.6 ? -_kFlingVelocity : _kFlingVelocity, + ); + } + } + + bool _handleScrollNotification(ScrollNotification notification) { + if (notification.depth == 0) { + if (notification is UserScrollNotification) { + switch (notification.direction) { + case ScrollDirection.forward: + _bottomAppBarController.forward(); + case ScrollDirection.reverse: + _bottomAppBarController.reverse(); + case ScrollDirection.idle: + break; + } + } + } + return false; + } + + Widget _buildStack(BuildContext context, BoxConstraints constraints) { + final ui.Size drawerSize = constraints.biggest; + final double drawerTop = drawerSize.height; + + final Animation drawerAnimation = RelativeRectTween( + begin: RelativeRect.fromLTRB(0.0, drawerTop, 0.0, 0.0), + end: RelativeRect.fill, + ).animate(_drawerCurve); + + return Stack( + clipBehavior: Clip.none, + key: _bottomDrawerKey, + children: [ + NotificationListener( + onNotification: _handleScrollNotification, + child: const _MailNavigator( + child: MailboxBody(), + ), + ), + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + _drawerController.reverse(); + _dropArrowController.reverse(); + }, + child: Visibility( + maintainAnimation: true, + maintainState: true, + visible: _bottomDrawerVisible, + child: FadeTransition( + opacity: _drawerCurve, + child: Container( + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width, + color: + Theme.of(context).bottomSheetTheme.modalBackgroundColor, + ), + ), + ), + ), + ), + PositionedTransition( + rect: drawerAnimation, + child: Visibility( + visible: _bottomDrawerVisible, + child: BottomDrawer( + onVerticalDragUpdate: _handleDragUpdate, + onVerticalDragEnd: _handleDragEnd, + leading: Consumer( + builder: (BuildContext context, EmailStore model, Widget? child) { + return _BottomDrawerDestinations( + destinations: widget.destinations, + drawerController: _drawerController, + dropArrowController: _dropArrowController, + selectedMailbox: model.selectedMailboxPage, + onItemTapped: widget.onItemTapped, + ); + }, + ), + trailing: _BottomDrawerFolderSection(folders: widget.folders), + ), + ), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + return _SharedAxisTransitionSwitcher( + defaultChild: Scaffold( + extendBody: true, + body: LayoutBuilder( + builder: _buildStack, + ), + bottomNavigationBar: Consumer( + builder: (BuildContext context, EmailStore model, Widget? child) { + return _AnimatedBottomAppBar( + bottomAppBarController: _bottomAppBarController, + bottomAppBarCurve: _bottomAppBarCurve, + bottomDrawerVisible: _bottomDrawerVisible, + drawerController: _drawerController, + dropArrowCurve: _dropArrowCurve, + navigationDestinations: widget.destinations, + selectedMailbox: model.selectedMailboxPage, + toggleBottomDrawerVisibility: _toggleBottomDrawerVisibility, + ); + }, + ), + floatingActionButton: _bottomDrawerVisible + ? null + : const Padding( + padding: EdgeInsetsDirectional.only(bottom: 8), + child: _ReplyFab(), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, + ), + ); + } +} + +class _AnimatedBottomAppBar extends StatelessWidget { + const _AnimatedBottomAppBar({ + required this.bottomAppBarController, + required this.bottomAppBarCurve, + required this.bottomDrawerVisible, + required this.drawerController, + required this.dropArrowCurve, + required this.navigationDestinations, + this.selectedMailbox, + this.toggleBottomDrawerVisibility, + }); + + final AnimationController bottomAppBarController; + final Animation bottomAppBarCurve; + final bool bottomDrawerVisible; + final AnimationController drawerController; + final Animation dropArrowCurve; + final List<_Destination> navigationDestinations; + final MailboxPageType? selectedMailbox; + final ui.VoidCallback? toggleBottomDrawerVisibility; + + @override + Widget build(BuildContext context) { + final Animation fadeOut = Tween(begin: 1, end: -1).animate( + drawerController.drive(CurveTween(curve: Easing.legacy)), + ); + + return Selector( + selector: (BuildContext context, EmailStore emailStore) => emailStore.onMailView, + builder: (BuildContext context, bool onMailView, Widget? child) { + bottomAppBarController.forward(); + + return SizeTransition( + sizeFactor: bottomAppBarCurve, + axisAlignment: -1, + child: Padding( + padding: const EdgeInsetsDirectional.only(top: 2), + child: BottomAppBar( + shape: const WaterfallNotchedRectangle(), + notchMargin: 6, + child: Container( + color: Colors.transparent, + height: kToolbarHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + InkWell( + key: const ValueKey('navigation_button'), + borderRadius: const BorderRadius.all(Radius.circular(16)), + onTap: toggleBottomDrawerVisibility, + child: Row( + children: [ + const SizedBox(width: 16), + RotationTransition( + turns: Tween( + begin: 0.0, + end: 1.0, + ).animate(dropArrowCurve), + child: const Icon( + Icons.arrow_drop_up, + color: ReplyColors.white50, + ), + ), + const SizedBox(width: 8), + const _ReplyLogo(), + const SizedBox(width: 10), + _FadeThroughTransitionSwitcher( + fillColor: Colors.transparent, + child: onMailView + ? const SizedBox(width: 48) + : FadeTransition( + opacity: fadeOut, + child: Text( + navigationDestinations + .firstWhere((_Destination destination) { + return destination.type == + selectedMailbox; + }).textLabel, + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(color: ReplyColors.white50), + ), + ), + ), + ], + ), + ), + Expanded( + child: ColoredBox( + color: Colors.transparent, + child: _BottomAppBarActionItems( + drawerVisible: bottomDrawerVisible, + ), + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } +} + +class _BottomAppBarActionItems extends StatelessWidget { + const _BottomAppBarActionItems({required this.drawerVisible}); + + final bool drawerVisible; + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (BuildContext context, EmailStore model, Widget? child) { + final bool onMailView = model.onMailView; + Color? starIconColor; + + if (onMailView) { + starIconColor = model.isCurrentEmailStarred + ? Theme.of(context).colorScheme.secondary + : ReplyColors.white50; + } + + return _FadeThroughTransitionSwitcher( + fillColor: Colors.transparent, + child: drawerVisible + ? Align( + key: UniqueKey(), + alignment: Alignment.centerRight, + child: IconButton( + icon: const Icon(Icons.settings), + color: ReplyColors.white50, + onPressed: () {}, + ), + ) + : onMailView + ? Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + key: const ValueKey('star_email_button'), + icon: ImageIcon( + const AssetImage( + '$_iconAssetLocation/twotone_star.png', + package: _assetsPackage, + ), + color: starIconColor, + ), + onPressed: () { + final Email currentEmail = model.currentEmail; + if (model.isCurrentEmailStarred) { + model.unstarEmail(currentEmail.id); + } else { + model.starEmail(currentEmail.id); + } + if (model.selectedMailboxPage == + MailboxPageType.starred) { + mobileMailNavKey.currentState!.pop(); + model.selectedEmailId = -1; + } + }, + color: ReplyColors.white50, + ), + IconButton( + icon: const ImageIcon( + AssetImage( + '$_iconAssetLocation/twotone_delete.png', + package: _assetsPackage, + ), + ), + onPressed: () { + model.deleteEmail( + model.selectedEmailId, + ); + + mobileMailNavKey.currentState!.pop(); + model.selectedEmailId = -1; + }, + color: ReplyColors.white50, + ), + IconButton( + icon: const Icon(Icons.more_vert), + onPressed: () {}, + color: ReplyColors.white50, + ), + ], + ) + : Align( + alignment: Alignment.centerRight, + child: IconButton( + key: const ValueKey('ReplySearch'), + icon: const Icon(Icons.search), + color: ReplyColors.white50, + onPressed: () { + Provider.of( + context, + listen: false, + ).onSearchPage = true; + }, + ), + ), + ); + }, + ); + } +} + +class _BottomDrawerDestinations extends StatelessWidget { + const _BottomDrawerDestinations({ + required this.destinations, + required this.drawerController, + required this.dropArrowController, + required this.selectedMailbox, + required this.onItemTapped, + }); + + final List<_Destination> destinations; + final AnimationController drawerController; + final AnimationController dropArrowController; + final MailboxPageType selectedMailbox; + final void Function(int, MailboxPageType) onItemTapped; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final List destinationButtons = []; + + for (int index = 0; index < destinations.length; index += 1) { + final _Destination destination = destinations[index]; + destinationButtons.add( + InkWell( + key: ValueKey('Reply-${destination.textLabel}'), + onTap: () { + drawerController.reverse(); + dropArrowController.forward(); + Future.delayed( + Duration( + milliseconds: (drawerController.value == 1 ? 300 : 120) * + GalleryOptions.of(context).timeDilation.toInt(), + ), + () { + // Wait until animations are complete to reload the state. + // Delay scales with the timeDilation value of the gallery. + onItemTapped(index, destination.type); + }, + ); + }, + child: ListTile( + mouseCursor: SystemMouseCursors.click, + leading: ImageIcon( + AssetImage( + destination.icon, + package: _assetsPackage, + ), + color: destination.type == selectedMailbox + ? theme.colorScheme.secondary + : theme.navigationRailTheme.unselectedLabelTextStyle!.color, + ), + title: Text( + destination.textLabel, + style: theme.textTheme.bodyMedium!.copyWith( + color: destination.type == selectedMailbox + ? theme.colorScheme.secondary + : theme.navigationRailTheme.unselectedLabelTextStyle!.color, + ), + ), + ), + ), + ); + } + + return Column( + children: destinationButtons, + ); + } +} + +class _Destination { + const _Destination({ + required this.type, + required this.textLabel, + required this.icon, + }); + + // Which mailbox page to display. For example, 'Starred' or 'Trash'. + final MailboxPageType type; + + // The localized text label for the inbox. + final String textLabel; + + // The icon that appears next to the text label for the inbox. + final String icon; +} + +class _BottomDrawerFolderSection extends StatelessWidget { + const _BottomDrawerFolderSection({required this.folders}); + + final Map folders; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final NavigationRailThemeData navigationRailTheme = theme.navigationRailTheme; + + return Column( + children: [ + for (final String folder in folders.keys) + InkWell( + onTap: () {}, + child: ListTile( + mouseCursor: SystemMouseCursors.click, + leading: ImageIcon( + AssetImage( + folders[folder]!, + package: _assetsPackage, + ), + color: navigationRailTheme.unselectedLabelTextStyle!.color, + ), + title: Text( + folder, + style: theme.textTheme.bodyMedium!.copyWith( + color: navigationRailTheme.unselectedLabelTextStyle!.color, + ), + ), + ), + ), + ], + ); + } +} + +class _MailNavigator extends StatefulWidget { + const _MailNavigator({ + required this.child, + }); + + final Widget child; + + @override + _MailNavigatorState createState() => _MailNavigatorState(); +} + +class _MailNavigatorState extends State<_MailNavigator> { + static const String inboxRoute = '/reply/inbox'; + + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + + return Navigator( + restorationScopeId: 'replyMailNavigator', + key: isDesktop ? desktopMailNavKey : mobileMailNavKey, + initialRoute: inboxRoute, + onGenerateRoute: (RouteSettings settings) { + switch (settings.name) { + case inboxRoute: + return MaterialPageRoute( + builder: (BuildContext context) { + return _FadeThroughTransitionSwitcher( + fillColor: Theme.of(context).scaffoldBackgroundColor, + child: widget.child, + ); + }, + settings: settings, + ); + case ReplyApp.composeRoute: + return ReplyApp.createComposeRoute(settings); + } + return null; + }, + ); + } +} + +class _ReplyLogo extends StatelessWidget { + const _ReplyLogo(); + + @override + Widget build(BuildContext context) { + return const ImageIcon( + AssetImage( + 'reply/reply_logo.png', + package: _assetsPackage, + ), + size: 32, + color: ReplyColors.white50, + ); + } +} + +class _ReplyFab extends StatefulWidget { + const _ReplyFab({this.extended = false}); + + final bool extended; + + @override + _ReplyFabState createState() => _ReplyFabState(); +} + +class _ReplyFabState extends State<_ReplyFab> + with SingleTickerProviderStateMixin { + static final UniqueKey fabKey = UniqueKey(); + static const double _mobileFabDimension = 56; + + void onPressed() { + final bool onSearchPage = Provider.of( + context, + listen: false, + ).onSearchPage; + // Navigator does not have an easy way to access the current + // route when using a GlobalKey to keep track of NavigatorState. + // We can use [Navigator.popUntil] in order to access the current + // route, and check if it is a ComposePage. If it is not a + // ComposePage and we are not on the SearchPage, then we can push + // a ComposePage onto our navigator. We return true at the end + // so nothing is popped. + desktopMailNavKey.currentState!.popUntil( + (Route route) { + final String? currentRoute = route.settings.name; + if (currentRoute != ReplyApp.composeRoute && !onSearchPage) { + desktopMailNavKey.currentState! + .restorablePushNamed(ReplyApp.composeRoute); + } + return true; + }, + ); + } + + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + final ThemeData theme = Theme.of(context); + const CircleBorder circleFabBorder = CircleBorder(); + + return Selector( + selector: (BuildContext context, EmailStore emailStore) => emailStore.onMailView, + builder: (BuildContext context, bool onMailView, Widget? child) { + final _FadeThroughTransitionSwitcher fabSwitcher = _FadeThroughTransitionSwitcher( + fillColor: Colors.transparent, + child: onMailView + ? Icon( + Icons.reply_all, + key: fabKey, + color: Colors.black, + ) + : const Icon( + Icons.create, + color: Colors.black, + ), + ); + final String tooltip = onMailView ? 'Reply' : 'Compose'; + + if (isDesktop) { + final Animation animation = NavigationRail.extendedAnimation(context); + return Container( + height: 56, + padding: EdgeInsets.symmetric( + vertical: ui.lerpDouble(0, 6, animation.value)!, + ), + child: animation.value == 0 + ? FloatingActionButton( + tooltip: tooltip, + key: const ValueKey('ReplyFab'), + onPressed: onPressed, + child: fabSwitcher, + ) + : Align( + alignment: AlignmentDirectional.centerStart, + child: FloatingActionButton.extended( + key: const ValueKey('ReplyFab'), + label: Row( + children: [ + fabSwitcher, + SizedBox(width: 16 * animation.value), + Align( + alignment: AlignmentDirectional.centerStart, + widthFactor: animation.value, + child: Text( + tooltip.toUpperCase(), + style: Theme.of(context) + .textTheme + .headlineSmall! + .copyWith( + fontSize: 16, + color: theme.colorScheme.onSecondary, + ), + ), + ), + ], + ), + onPressed: onPressed, + ), + ), + ); + } else { + // TODO(x): State restoration of compose page on mobile is blocked because OpenContainer does not support restorablePush, https://github.com/flutter/gallery/issues/570. + return OpenContainer( + openBuilder: (BuildContext context, void Function() closedContainer) { + return const ComposePage(); + }, + openColor: theme.cardColor, + closedShape: circleFabBorder, + closedColor: theme.colorScheme.secondary, + closedElevation: 6, + closedBuilder: (BuildContext context, void Function() openContainer) { + return Tooltip( + message: tooltip, + child: InkWell( + key: const ValueKey('ReplyFab'), + customBorder: circleFabBorder, + onTap: openContainer, + child: SizedBox( + height: _mobileFabDimension, + width: _mobileFabDimension, + child: Center( + child: fabSwitcher, + ), + ), + ), + ); + }, + ); + } + }, + ); + } +} + +class _FadeThroughTransitionSwitcher extends StatelessWidget { + const _FadeThroughTransitionSwitcher({ + required this.fillColor, + required this.child, + }); + + final Widget child; + final Color fillColor; + + @override + Widget build(BuildContext context) { + return PageTransitionSwitcher( + transitionBuilder: (Widget child, Animation animation, Animation secondaryAnimation) { + return FadeThroughTransition( + fillColor: fillColor, + animation: animation, + secondaryAnimation: secondaryAnimation, + child: child, + ); + }, + child: child, + ); + } +} + +class _SharedAxisTransitionSwitcher extends StatelessWidget { + const _SharedAxisTransitionSwitcher({required this.defaultChild}); + + final Widget defaultChild; + + @override + Widget build(BuildContext context) { + return Selector( + selector: (BuildContext context, EmailStore emailStore) => emailStore.onSearchPage, + builder: (BuildContext context, bool onSearchPage, Widget? child) { + return PageTransitionSwitcher( + reverse: !onSearchPage, + transitionBuilder: (Widget child, Animation animation, Animation secondaryAnimation) { + return SharedAxisTransition( + fillColor: Theme.of(context).colorScheme.background, + animation: animation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.scaled, + child: child, + ); + }, + child: onSearchPage ? const SearchPage() : defaultChild, + ); + }, + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/reply/app.dart b/dev/integration_tests/new_gallery/lib/studies/reply/app.dart new file mode 100644 index 0000000000..f27fb0b130 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/reply/app.dart @@ -0,0 +1,352 @@ +// 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) { + case ReplyApp.homeRoute: + return MaterialPageRoute( + builder: (BuildContext context) => const AdaptiveNav(), + settings: settings, + ); + case ReplyApp.composeRoute: + return ReplyApp.createComposeRoute(settings); + } + return 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, + ), + ); +} diff --git a/dev/integration_tests/new_gallery/lib/studies/reply/bottom_drawer.dart b/dev/integration_tests/new_gallery/lib/studies/reply/bottom_drawer.dart new file mode 100644 index 0000000000..e6b34e7ab3 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/reply/bottom_drawer.dart @@ -0,0 +1,67 @@ +// 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:flutter/material.dart'; +import 'colors.dart'; + +class BottomDrawer extends StatelessWidget { + const BottomDrawer({ + super.key, + this.onVerticalDragUpdate, + this.onVerticalDragEnd, + required this.leading, + required this.trailing, + }); + + final GestureDragUpdateCallback? onVerticalDragUpdate; + final GestureDragEndCallback? onVerticalDragEnd; + final Widget leading; + final Widget trailing; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onVerticalDragUpdate: onVerticalDragUpdate, + onVerticalDragEnd: onVerticalDragEnd, + child: Material( + color: theme.bottomSheetTheme.backgroundColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + child: ListView( + padding: const EdgeInsets.all(12), + physics: const NeverScrollableScrollPhysics(), + children: [ + const SizedBox(height: 28), + leading, + const SizedBox(height: 8), + const Divider( + color: ReplyColors.blue200, + thickness: 0.25, + indent: 18, + endIndent: 160, + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsetsDirectional.only(start: 18), + child: Text( + 'FOLDERS', + style: theme.textTheme.bodySmall!.copyWith( + color: + theme.navigationRailTheme.unselectedLabelTextStyle!.color, + ), + ), + ), + const SizedBox(height: 4), + trailing, + ], + ), + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/reply/colors.dart b/dev/integration_tests/new_gallery/lib/studies/reply/colors.dart new file mode 100644 index 0000000000..b419eccb5d --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/reply/colors.dart @@ -0,0 +1,42 @@ +// 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:flutter/material.dart'; + +class ReplyColors { + static const Color white50 = Color(0xFFFFFFFF); + + static const Color black800 = Color(0xFF121212); + static const Color black900 = Color(0xFF000000); + + static const Color blue50 = Color(0xFFEEF0F2); + static const Color blue100 = Color(0xFFD2DBE0); + static const Color blue200 = Color(0xFFADBBC4); + static const Color blue300 = Color(0xFF8CA2AE); + static const Color blue600 = Color(0xFF4A6572); + static const Color blue700 = Color(0xFF344955); + static const Color blue800 = Color(0xFF232F34); + + static const Color orange300 = Color(0xFFFBD790); + static const Color orange400 = Color(0xFFF9BE64); + static const Color orange500 = Color(0xFFF9AA33); + + static const Color red200 = Color(0xFFCF7779); + static const Color red400 = Color(0xFFFF4C5D); + + static const Color white50Alpha060 = Color(0x99FFFFFF); + + static const Color blue50Alpha060 = Color(0x99EEF0F2); + + static const Color black900Alpha020 = Color(0x33000000); + static const Color black900Alpha087 = Color(0xDE000000); + static const Color black900Alpha060 = Color(0x99000000); + + static const Color greyLabel = Color(0xFFAEAEAE); + static const Color darkBottomAppBarBackground = Color(0xFF2D2D2D); + static const Color darkDrawerBackground = Color(0xFF353535); + static const Color darkCardBackground = Color(0xFF1E1E1E); + static const Color darkChipBackground = Color(0xFF2A2A2A); + static const Color lightChipBackground = Color(0xFFE5E5E5); +} diff --git a/dev/integration_tests/new_gallery/lib/studies/reply/compose_page.dart b/dev/integration_tests/new_gallery/lib/studies/reply/compose_page.dart new file mode 100644 index 0000000000..ad93d9b09e --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/reply/compose_page.dart @@ -0,0 +1,282 @@ +// 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:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'model/email_model.dart'; +import 'model/email_store.dart'; + +class ComposePage extends StatelessWidget { + const ComposePage({super.key}); + + @override + Widget build(BuildContext context) { + const String senderEmail = 'flutterfan@gmail.com'; + String subject = ''; + String? recipient = 'Recipient'; + String recipientAvatar = 'reply/avatars/avatar_0.jpg'; + + final EmailStore emailStore = Provider.of(context); + + if (emailStore.selectedEmailId >= 0) { + final Email currentEmail = emailStore.currentEmail; + subject = currentEmail.subject; + recipient = currentEmail.sender; + recipientAvatar = currentEmail.avatar; + } + + return Scaffold( + body: SafeArea( + bottom: false, + child: SizedBox( + height: double.infinity, + child: Material( + color: Theme.of(context).cardColor, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _SubjectRow( + subject: subject, + ), + const _SectionDivider(), + const _SenderAddressRow( + senderEmail: senderEmail, + ), + const _SectionDivider(), + _RecipientsRow( + recipients: recipient, + avatar: recipientAvatar, + ), + const _SectionDivider(), + Padding( + padding: const EdgeInsets.all(12), + child: TextField( + minLines: 6, + maxLines: 20, + decoration: const InputDecoration.collapsed( + hintText: 'New Message...', + ), + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} + +class _SubjectRow extends StatefulWidget { + const _SubjectRow({required this.subject}); + + final String subject; + + @override + _SubjectRowState createState() => _SubjectRowState(); +} + +class _SubjectRowState extends State<_SubjectRow> { + TextEditingController? _subjectController; + + @override + void initState() { + super.initState(); + _subjectController = TextEditingController(text: widget.subject); + } + + @override + void dispose() { + _subjectController!.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final ColorScheme colorScheme = theme.colorScheme; + + return Padding( + padding: const EdgeInsets.only(top: 8), + child: Row( + children: [ + IconButton( + key: const ValueKey('ReplyExit'), + onPressed: () => Navigator.of(context).pop(), + icon: Icon( + Icons.close, + color: colorScheme.onSurface, + ), + ), + Expanded( + child: TextField( + controller: _subjectController, + style: theme.textTheme.titleLarge, + decoration: InputDecoration.collapsed( + hintText: 'Subject', + hintStyle: theme.textTheme.titleLarge!.copyWith( + color: theme.colorScheme.primary.withOpacity(0.5), + ), + ), + ), + ), + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: IconButton( + icon: ImageIcon( + const AssetImage( + 'reply/icons/twotone_send.png', + package: 'flutter_gallery_assets', + ), + color: colorScheme.onSurface, + ), + onPressed: () => Navigator.of(context).pop(), + ), + ), + ], + ), + ); + } +} + +class _SenderAddressRow extends StatefulWidget { + const _SenderAddressRow({required this.senderEmail}); + + final String senderEmail; + + @override + __SenderAddressRowState createState() => __SenderAddressRowState(); +} + +class __SenderAddressRowState extends State<_SenderAddressRow> { + late String senderEmail; + + @override + void initState() { + super.initState(); + senderEmail = widget.senderEmail; + } + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final TextTheme textTheme = theme.textTheme; + final List accounts = [ + 'flutterfan@gmail.com', + 'materialfan@gmail.com', + ]; + + return PopupMenuButton( + padding: EdgeInsets.zero, + onSelected: (String email) { + setState(() { + senderEmail = email; + }); + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: accounts[0], + child: Text( + accounts[0], + style: textTheme.bodyMedium, + ), + ), + PopupMenuItem( + value: accounts[1], + child: Text( + accounts[1], + style: textTheme.bodyMedium, + ), + ), + ], + child: Padding( + padding: const EdgeInsets.only( + left: 12, + top: 16, + right: 10, + bottom: 10, + ), + child: Row( + children: [ + Expanded( + child: Text( + senderEmail, + style: textTheme.bodyMedium, + ), + ), + Icon( + Icons.arrow_drop_down, + color: theme.colorScheme.onSurface, + ), + ], + ), + ), + ); + } +} + +class _RecipientsRow extends StatelessWidget { + const _RecipientsRow({ + required this.recipients, + required this.avatar, + }); + + final String recipients; + final String avatar; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12), + child: Row( + children: [ + Expanded( + child: Wrap( + children: [ + Chip( + backgroundColor: + Theme.of(context).chipTheme.secondarySelectedColor, + padding: EdgeInsets.zero, + avatar: CircleAvatar( + backgroundImage: AssetImage( + avatar, + package: 'flutter_gallery_assets', + ), + ), + label: Text( + recipients, + ), + ), + ], + ), + ), + InkResponse( + customBorder: const CircleBorder(), + onTap: () {}, + radius: 24, + child: const Icon(Icons.add_circle_outline), + ), + ], + ), + ); + } +} + +class _SectionDivider extends StatelessWidget { + const _SectionDivider(); + + @override + Widget build(BuildContext context) { + return const Divider( + thickness: 1.1, + indent: 10, + endIndent: 10, + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/reply/mail_card_preview.dart b/dev/integration_tests/new_gallery/lib/studies/reply/mail_card_preview.dart new file mode 100644 index 0000000000..86058ca555 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/reply/mail_card_preview.dart @@ -0,0 +1,358 @@ +// 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/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../layout/adaptive.dart'; +import 'colors.dart'; +import 'mail_view_page.dart'; +import 'model/email_model.dart'; +import 'model/email_store.dart'; +import 'profile_avatar.dart'; + +const String _assetsPackage = 'flutter_gallery_assets'; +const String _iconAssetLocation = 'reply/icons'; + +class MailPreviewCard extends StatelessWidget { + const MailPreviewCard({ + super.key, + required this.id, + required this.email, + required this.onDelete, + required this.onStar, + required this.isStarred, + required this.onStarredMailbox, + }); + + final int id; + final Email email; + final VoidCallback onDelete; + final VoidCallback onStar; + final bool isStarred; + final bool onStarredMailbox; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + + // TODO(x): State restoration of mail view page is blocked because OpenContainer does not support restorablePush, https://github.com/flutter/gallery/issues/570. + return OpenContainer( + openBuilder: (BuildContext context, void Function() closedContainer) { + return MailViewPage(id: id, email: email); + }, + openColor: theme.cardColor, + closedShape: const RoundedRectangleBorder( + + ), + closedElevation: 0, + closedColor: theme.cardColor, + closedBuilder: (BuildContext context, void Function() openContainer) { + final bool isDesktop = isDisplayDesktop(context); + final ColorScheme colorScheme = theme.colorScheme; + final _MailPreview mailPreview = _MailPreview( + id: id, + email: email, + onTap: openContainer, + onStar: onStar, + onDelete: onDelete, + ); + + if (isDesktop) { + return mailPreview; + } else { + return Dismissible( + key: ObjectKey(email), + dismissThresholds: const { + DismissDirection.startToEnd: 0.8, + DismissDirection.endToStart: 0.4, + }, + onDismissed: (DismissDirection direction) { + switch (direction) { + case DismissDirection.endToStart: + if (onStarredMailbox) { + onStar(); + } + case DismissDirection.startToEnd: + onDelete(); + case DismissDirection.vertical: + case DismissDirection.horizontal: + case DismissDirection.up: + case DismissDirection.down: + case DismissDirection.none: + break; + } + }, + background: _DismissibleContainer( + icon: 'twotone_delete', + backgroundColor: colorScheme.primary, + iconColor: ReplyColors.blue50, + alignment: Alignment.centerLeft, + padding: const EdgeInsetsDirectional.only(start: 20), + ), + confirmDismiss: (DismissDirection direction) async { + if (direction == DismissDirection.endToStart) { + if (onStarredMailbox) { + return true; + } + onStar(); + return false; + } else { + return true; + } + }, + secondaryBackground: _DismissibleContainer( + icon: 'twotone_star', + backgroundColor: isStarred + ? colorScheme.secondary + : theme.scaffoldBackgroundColor, + iconColor: isStarred + ? colorScheme.onSecondary + : colorScheme.onBackground, + alignment: Alignment.centerRight, + padding: const EdgeInsetsDirectional.only(end: 20), + ), + child: mailPreview, + ); + } + }, + ); + } +} + +class _DismissibleContainer extends StatelessWidget { + const _DismissibleContainer({ + required this.icon, + required this.backgroundColor, + required this.iconColor, + required this.alignment, + required this.padding, + }); + + final String icon; + final Color backgroundColor; + final Color iconColor; + final Alignment alignment; + final EdgeInsetsDirectional padding; + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + alignment: alignment, + curve: Easing.legacy, + color: backgroundColor, + duration: kThemeAnimationDuration, + padding: padding, + child: Material( + color: Colors.transparent, + child: ImageIcon( + AssetImage( + 'reply/icons/$icon.png', + package: 'flutter_gallery_assets', + ), + size: 36, + color: iconColor, + ), + ), + ); + } +} + +class _MailPreview extends StatelessWidget { + const _MailPreview({ + required this.id, + required this.email, + required this.onTap, + this.onStar, + this.onDelete, + }); + + final int id; + final Email email; + final VoidCallback onTap; + final VoidCallback? onStar; + final VoidCallback? onDelete; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + final EmailStore emailStore = Provider.of( + context, + listen: false, + ); + + return InkWell( + onTap: () { + Provider.of( + context, + listen: false, + ).selectedEmailId = id; + onTap(); + }, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return ConstrainedBox( + constraints: BoxConstraints(maxHeight: constraints.maxHeight), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + '${email.sender} - ${email.time}', + style: textTheme.bodySmall, + ), + const SizedBox(height: 4), + Text(email.subject, style: textTheme.headlineSmall), + const SizedBox(height: 16), + ], + ), + ), + _MailPreviewActionBar( + avatar: email.avatar, + isStarred: emailStore.isEmailStarred(email.id), + onStar: onStar, + onDelete: onDelete, + ), + ], + ), + Padding( + padding: const EdgeInsetsDirectional.only( + end: 20, + ), + child: Text( + email.message, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: textTheme.bodyMedium, + ), + ), + if (email.containsPictures) ...[ + const Flexible( + child: Column( + children: [ + SizedBox(height: 20), + _PicturePreview(), + ], + ), + ), + ], + ], + ), + ), + ); + }, + ), + ); + } +} + +class _PicturePreview extends StatelessWidget { + const _PicturePreview(); + + bool _shouldShrinkImage() { + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + case TargetPlatform.android: + return true; + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.macOS: + case TargetPlatform.windows: + return false; + } + } + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 96, + child: ListView.builder( + itemCount: 4, + scrollDirection: Axis.horizontal, + itemBuilder: (BuildContext context, int index) { + return Padding( + padding: const EdgeInsetsDirectional.only(end: 4), + child: Image.asset( + 'reply/attachments/paris_${index + 1}.jpg', + gaplessPlayback: true, + package: 'flutter_gallery_assets', + cacheWidth: _shouldShrinkImage() ? 200 : null, + ), + ); + }, + ), + ); + } +} + +class _MailPreviewActionBar extends StatelessWidget { + const _MailPreviewActionBar({ + required this.avatar, + required this.isStarred, + this.onStar, + this.onDelete, + }); + + final String avatar; + final bool isStarred; + final VoidCallback? onStar; + final VoidCallback? onDelete; + + @override + Widget build(BuildContext context) { + final bool isDark = Theme.of(context).brightness == Brightness.dark; + final Color color = isDark ? ReplyColors.white50 : ReplyColors.blue600; + final bool isDesktop = isDisplayDesktop(context); + final Color starredIconColor = + isStarred ? Theme.of(context).colorScheme.secondary : color; + + return Row( + children: [ + if (isDesktop) ...[ + IconButton( + icon: ImageIcon( + const AssetImage( + '$_iconAssetLocation/twotone_star.png', + package: _assetsPackage, + ), + color: starredIconColor, + ), + onPressed: onStar, + ), + IconButton( + icon: ImageIcon( + const AssetImage( + '$_iconAssetLocation/twotone_delete.png', + package: _assetsPackage, + ), + color: color, + ), + onPressed: onDelete, + ), + IconButton( + icon: Icon( + Icons.more_vert, + color: color, + ), + onPressed: () {}, + ), + const SizedBox(width: 12), + ], + ProfileAvatar(avatar: avatar), + ], + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/reply/mail_view_page.dart b/dev/integration_tests/new_gallery/lib/studies/reply/mail_view_page.dart new file mode 100644 index 0000000000..914394e813 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/reply/mail_view_page.dart @@ -0,0 +1,178 @@ +// 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:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'model/email_model.dart'; +import 'model/email_store.dart'; +import 'profile_avatar.dart'; + +class MailViewPage extends StatelessWidget { + const MailViewPage({ + super.key, + required this.id, + required this.email, + }); + + final int id; + final Email email; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + bottom: false, + child: SizedBox( + height: double.infinity, + child: Material( + color: Theme.of(context).cardColor, + child: SingleChildScrollView( + padding: const EdgeInsetsDirectional.only( + top: 42, + start: 20, + end: 20, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _MailViewHeader(email: email), + const SizedBox(height: 32), + _MailViewBody(message: email.message), + if (email.containsPictures) ...[ + const SizedBox(height: 28), + const _PictureGrid(), + ], + const SizedBox(height: kToolbarHeight), + ], + ), + ), + ), + ), + ), + ); + } +} + +class _MailViewHeader extends StatelessWidget { + const _MailViewHeader({required this.email}); + + final Email email; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + + return Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: SelectableText( + email.subject, + style: textTheme.headlineMedium!.copyWith(height: 1.1), + ), + ), + IconButton( + key: const ValueKey('ReplyExit'), + icon: const Icon(Icons.keyboard_arrow_down), + onPressed: () { + Provider.of( + context, + listen: false, + ).selectedEmailId = -1; + Navigator.pop(context); + }, + splashRadius: 20, + ), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + SelectableText('${email.sender} - ${email.time}'), + const SizedBox(height: 4), + SelectableText( + 'To ${email.recipients},', + style: textTheme.bodySmall!.copyWith( + color: Theme.of(context) + .navigationRailTheme + .unselectedLabelTextStyle! + .color, + ), + ), + ], + ), + Padding( + padding: const EdgeInsetsDirectional.only(end: 4), + child: ProfileAvatar(avatar: email.avatar), + ), + ], + ), + ], + ); + } +} + +class _MailViewBody extends StatelessWidget { + const _MailViewBody({required this.message}); + + final String message; + + @override + Widget build(BuildContext context) { + return SelectableText( + message, + style: Theme.of(context).textTheme.bodyMedium!.copyWith(fontSize: 16), + ); + } +} + +class _PictureGrid extends StatelessWidget { + const _PictureGrid(); + + bool _shouldShrinkImage() { + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + case TargetPlatform.android: + return true; + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.macOS: + case TargetPlatform.windows: + return false; + } + } + + @override + Widget build(BuildContext context) { + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 4, + mainAxisSpacing: 4, + ), + itemCount: 4, + itemBuilder: (BuildContext context, int index) { + return Image.asset( + 'reply/attachments/paris_${index + 1}.jpg', + gaplessPlayback: true, + package: 'flutter_gallery_assets', + fit: BoxFit.fill, + cacheWidth: _shouldShrinkImage() ? 500 : null, + ); + }, + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/reply/mailbox_body.dart b/dev/integration_tests/new_gallery/lib/studies/reply/mailbox_body.dart new file mode 100644 index 0000000000..75b59999e3 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/reply/mailbox_body.dart @@ -0,0 +1,138 @@ +// 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:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../layout/adaptive.dart'; +import 'mail_card_preview.dart'; +import 'model/email_model.dart'; +import 'model/email_store.dart'; + +class MailboxBody extends StatelessWidget { + const MailboxBody({super.key}); + + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + final bool isTablet = isDisplaySmallDesktop(context); + final double startPadding = isTablet + ? 60.0 + : isDesktop + ? 120.0 + : 4.0; + final double endPadding = isTablet + ? 30.0 + : isDesktop + ? 60.0 + : 4.0; + + return Consumer( + builder: (BuildContext context, EmailStore model, Widget? child) { + final MailboxPageType destination = model.selectedMailboxPage; + final String destinationString = destination + .toString() + .substring(destination.toString().indexOf('.') + 1); + late List emails; + + switch (destination) { + case MailboxPageType.inbox: + { + emails = model.inboxEmails; + break; + } + case MailboxPageType.sent: + { + emails = model.outboxEmails; + break; + } + case MailboxPageType.starred: + { + emails = model.starredEmails; + break; + } + case MailboxPageType.trash: + { + emails = model.trashEmails; + break; + } + case MailboxPageType.spam: + { + emails = model.spamEmails; + break; + } + case MailboxPageType.drafts: + { + emails = model.draftEmails; + break; + } + } + + return SafeArea( + bottom: false, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: emails.isEmpty + ? Center(child: Text('Empty in $destinationString')) + : ListView.separated( + itemCount: emails.length, + padding: EdgeInsetsDirectional.only( + start: startPadding, + end: endPadding, + top: isDesktop ? 28 : 0, + bottom: kToolbarHeight, + ), + primary: false, + separatorBuilder: (BuildContext context, int index) => + const SizedBox(height: 4), + itemBuilder: (BuildContext context, int index) { + final Email email = emails[index]; + return MailPreviewCard( + id: email.id, + email: email, + isStarred: model.isEmailStarred(email.id), + onDelete: () => model.deleteEmail(email.id), + onStar: () { + final int emailId = email.id; + if (model.isEmailStarred(emailId)) { + model.unstarEmail(emailId); + } else { + model.starEmail(emailId); + } + }, + onStarredMailbox: model.selectedMailboxPage == + MailboxPageType.starred, + ); + }, + ), + ), + if (isDesktop) ...[ + Padding( + padding: const EdgeInsetsDirectional.only(top: 14), + child: Row( + children: [ + IconButton( + key: const ValueKey('ReplySearch'), + icon: const Icon(Icons.search), + onPressed: () { + Provider.of( + context, + listen: false, + ).onSearchPage = true; + }, + ), + SizedBox(width: isTablet ? 30 : 60), + ], + ), + ), + ] + ], + ), + ); + }, + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/reply/model/email_model.dart b/dev/integration_tests/new_gallery/lib/studies/reply/model/email_model.dart new file mode 100644 index 0000000000..5200fc6bf0 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/reply/model/email_model.dart @@ -0,0 +1,57 @@ +// 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. + +class Email { + Email({ + required this.id, + required this.avatar, + this.sender = '', + this.time = '', + this.subject = '', + this.message = '', + this.recipients = '', + this.containsPictures = false, + }); + + final int id; + final String sender; + final String time; + final String subject; + final String message; + final String avatar; + final String recipients; + final bool containsPictures; +} + +class InboxEmail extends Email { + InboxEmail({ + required super.id, + required super.sender, + super.time, + super.subject, + super.message, + required super.avatar, + super.recipients, + super.containsPictures, + this.inboxType = InboxType.normal, + }); + + InboxType inboxType; +} + +// The different mailbox pages that the Reply app contains. +enum MailboxPageType { + inbox, + starred, + sent, + trash, + spam, + drafts, +} + +// Different types of mail that can be sent to the inbox. +enum InboxType { + normal, + spam, +} diff --git a/dev/integration_tests/new_gallery/lib/studies/reply/model/email_store.dart b/dev/integration_tests/new_gallery/lib/studies/reply/model/email_store.dart new file mode 100644 index 0000000000..9afdb34726 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/reply/model/email_store.dart @@ -0,0 +1,253 @@ +// 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:flutter/widgets.dart'; + +import 'email_model.dart'; + +const String _avatarsLocation = 'reply/avatars'; + +class EmailStore with ChangeNotifier { + static final List _inbox = [ + InboxEmail( + id: 1, + sender: 'Google Express', + time: '15 minutes ago', + subject: 'Package shipped!', + message: 'Cucumber Mask Facial has shipped.\n\n' + "Keep an eye out for a package to arrive between this Thursday and next Tuesday. If for any reason you don't receive your package before the end of next week, please reach out to us for details on your shipment.\n\n" + 'As always, thank you for shopping with us and we hope you love our specially formulated Cucumber Mask!', + avatar: '$_avatarsLocation/avatar_express.png', + recipients: 'Jeff', + ), + InboxEmail( + id: 2, + sender: 'Ali Connors', + time: '4 hrs ago', + subject: 'Brunch this weekend?', + message: + "I'll be in your neighborhood doing errands and was hoping to catch you for a coffee this Saturday. If you don't have anything scheduled, it would be great to see you! It feels like its been forever.\n\n" + 'If we do get a chance to get together, remind me to tell you about Kim. She stopped over at the house to say hey to the kids and told me all about her trip to Mexico.\n\n' + 'Talk to you soon,\n\n' + 'Ali', + avatar: '$_avatarsLocation/avatar_5.jpg', + recipients: 'Jeff', + ), + InboxEmail( + id: 3, + sender: 'Allison Trabucco', + time: '5 hrs ago', + subject: 'Bonjour from Paris', + message: 'Here are some great shots from my trip...', + avatar: '$_avatarsLocation/avatar_3.jpg', + recipients: 'Jeff', + containsPictures: true, + ), + InboxEmail( + id: 4, + sender: 'Trevor Hansen', + time: '9 hrs ago', + subject: 'Brazil trip', + message: + 'Thought we might be able to go over some details about our upcoming vacation.\n\n' + "I've been doing a bit of research and have come across a few paces in Northern Brazil that I think we should check out. " + 'One, the north has some of the most predictable wind on the planet. ' + "I'd love to get out on the ocean and kitesurf for a couple of days if we're going to be anywhere near or around Taiba. " + "I hear it's beautiful there and if you're up for it, I'd love to go. Other than that, I haven't spent too much time looking into places along our road trip route. " + "I'm assuming we can find places to stay and things to do as we drive and find places we think look interesting. But... I know you're more of a planner, so if you have ideas or places in mind, lets jot some ideas down!\n\n" + 'Maybe we can jump on the phone later today if you have a second.', + avatar: '$_avatarsLocation/avatar_8.jpg', + recipients: 'Allison, Kim, Jeff', + ), + InboxEmail( + id: 5, + sender: 'Frank Hawkins', + time: '10 hrs ago', + subject: 'Update to Your Itinerary', + avatar: '$_avatarsLocation/avatar_4.jpg', + recipients: 'Jeff', + ), + InboxEmail( + id: 6, + sender: 'Google Express', + time: '12 hrs ago', + subject: 'Delivered', + message: 'Your shoes should be waiting for you at home!', + avatar: '$_avatarsLocation/avatar_express.png', + recipients: 'Jeff', + ), + InboxEmail( + id: 7, + sender: 'Frank Hawkins', + time: '4 hrs ago', + subject: 'Your update on the Google Play Store is live!', + message: + 'Your update is now live on the Play Store and available for your alpha users to start testing.\n\n' + "Your alpha testers will be automatically notified. If you'd rather send them a link directly, go to your Google Play Console and follow the instructions for obtaining an open alpha testing link.", + avatar: '$_avatarsLocation/avatar_4.jpg', + recipients: 'Jeff', + ), + InboxEmail( + id: 8, + sender: 'Allison Trabucco', + time: '6 hrs ago', + subject: 'Try a free TrailGo account', + message: + 'Looking for the best hiking trails in your area? TrailGo gets you on the path to the outdoors faster than you can pack a sandwich.\n\n' + "Whether you're an experienced hiker or just looking to get outside for the afternoon, there's a segment that suits you.", + avatar: '$_avatarsLocation/avatar_3.jpg', + recipients: 'Jeff', + ), + InboxEmail( + id: 9, + sender: 'Allison Trabucco', + time: '4 hrs ago', + subject: 'Free money', + message: + "You've been selected as a winner in our latest raffle! To claim your prize, click on the link.", + avatar: '$_avatarsLocation/avatar_3.jpg', + recipients: 'Jeff', + inboxType: InboxType.spam, + ), + ]; + + static final List _outbox = [ + Email( + id: 10, + sender: 'Kim Alen', + time: '4 hrs ago', + subject: 'High school reunion?', + message: + "Hi friends,\n\nI was at the grocery store on Sunday night.. when I ran into Genie Williams! I almost didn't recognize her afer 20 years!\n\n" + "Anyway, it turns out she is on the organizing committee for the high school reunion this fall. I don't know if you were planning on going or not, but she could definitely use our help in trying to track down lots of missing alums. " + "If you can make it, we're doing a little phone-tree party at her place next Saturday, hoping that if we can find one person, thee more will...", + avatar: '$_avatarsLocation/avatar_7.jpg', + recipients: 'Jeff', + ), + Email( + id: 11, + sender: 'Sandra Adams', + time: '7 hrs ago', + subject: 'Recipe to try', + message: + 'Raspberry Pie: We should make this pie recipe tonight! The filling is ' + 'very quick to put together.', + avatar: '$_avatarsLocation/avatar_2.jpg', + recipients: 'Jeff', + ), + ]; + + static final List _drafts = [ + Email( + id: 12, + sender: 'Sandra Adams', + time: '2 hrs ago', + subject: '(No subject)', + message: 'Hey,\n\n' + 'Wanted to email and see what you thought of', + avatar: '$_avatarsLocation/avatar_2.jpg', + recipients: 'Jeff', + ), + ]; + + List get _allEmails => [ + ..._inbox, + ..._outbox, + ..._drafts, + ]; + + List get inboxEmails { + return _inbox.where((Email email) { + if (email is InboxEmail) { + return email.inboxType == InboxType.normal && + !trashEmailIds.contains(email.id); + } + return false; + }).toList(); + } + + List get spamEmails { + return _inbox.where((Email email) { + if (email is InboxEmail) { + return email.inboxType == InboxType.spam && + !trashEmailIds.contains(email.id); + } + return false; + }).toList(); + } + + Email get currentEmail => + _allEmails.firstWhere((Email email) => email.id == _selectedEmailId); + + List get outboxEmails => + _outbox.where((Email email) => !trashEmailIds.contains(email.id)).toList(); + + List get draftEmails => + _drafts.where((Email email) => !trashEmailIds.contains(email.id)).toList(); + + Set starredEmailIds = {}; + + bool isEmailStarred(int id) => + _allEmails.any((Email email) => email.id == id && starredEmailIds.contains(id)); + + bool get isCurrentEmailStarred => starredEmailIds.contains(currentEmail.id); + + List get starredEmails { + return _allEmails + .where((Email email) => starredEmailIds.contains(email.id)) + .toList(); + } + + void starEmail(int id) { + starredEmailIds.add(id); + notifyListeners(); + } + + void unstarEmail(int id) { + starredEmailIds.remove(id); + notifyListeners(); + } + + Set trashEmailIds = {7, 8}; + + List get trashEmails { + return _allEmails + .where((Email email) => trashEmailIds.contains(email.id)) + .toList(); + } + + void deleteEmail(int id) { + trashEmailIds.add(id); + notifyListeners(); + } + + int _selectedEmailId = -1; + + int get selectedEmailId => _selectedEmailId; + + set selectedEmailId(int value) { + _selectedEmailId = value; + notifyListeners(); + } + + bool get onMailView => _selectedEmailId > -1; + + MailboxPageType _selectedMailboxPage = MailboxPageType.inbox; + + MailboxPageType get selectedMailboxPage => _selectedMailboxPage; + + set selectedMailboxPage(MailboxPageType mailboxPage) { + _selectedMailboxPage = mailboxPage; + notifyListeners(); + } + + bool _onSearchPage = false; + + bool get onSearchPage => _onSearchPage; + + set onSearchPage(bool value) { + _onSearchPage = value; + notifyListeners(); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/reply/profile_avatar.dart b/dev/integration_tests/new_gallery/lib/studies/reply/profile_avatar.dart new file mode 100644 index 0000000000..8be190ca8c --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/reply/profile_avatar.dart @@ -0,0 +1,37 @@ +// 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:flutter/material.dart'; + +class ProfileAvatar extends StatelessWidget { + const ProfileAvatar({ + super.key, + required this.avatar, + this.radius = 20, + }); + + final String avatar; + final double radius; + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: CircleAvatar( + radius: radius, + backgroundColor: Theme.of(context).cardColor, + child: ClipOval( + child: Image.asset( + avatar, + gaplessPlayback: true, + package: 'flutter_gallery_assets', + height: 42, + width: 42, + fit: BoxFit.cover, + ), + ), + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/reply/routes.dart b/dev/integration_tests/new_gallery/lib/studies/reply/routes.dart new file mode 100644 index 0000000000..7b26ba144e --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/reply/routes.dart @@ -0,0 +1,6 @@ +// 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. + +const String homeRoute = '/reply'; +const String composeRoute = '/reply/compose'; diff --git a/dev/integration_tests/new_gallery/lib/studies/reply/search_page.dart b/dev/integration_tests/new_gallery/lib/studies/reply/search_page.dart new file mode 100644 index 0000000000..0938d3376d --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/reply/search_page.dart @@ -0,0 +1,131 @@ +// 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:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'model/email_store.dart'; + +class SearchPage extends StatelessWidget { + const SearchPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Material( + color: Theme.of(context).colorScheme.surface, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + BackButton( + key: const ValueKey('ReplyExit'), + onPressed: () { + Provider.of( + context, + listen: false, + ).onSearchPage = false; + }, + ), + const Expanded( + child: TextField( + decoration: InputDecoration.collapsed( + hintText: 'Search email', + ), + ), + ), + IconButton( + icon: const Icon(Icons.mic), + onPressed: () {}, + ) + ], + ), + ), + const Divider(thickness: 1), + const Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _SectionHeader(title: 'YESTERDAY'), + _SearchHistoryTile( + search: '481 Van Brunt Street', + address: 'Brooklyn, NY', + ), + _SearchHistoryTile( + icon: Icons.home, + search: 'Home', + address: '199 Pacific Street, Brooklyn, NY', + ), + _SectionHeader(title: 'THIS WEEK'), + _SearchHistoryTile( + search: 'BEP GA', + address: 'Forsyth Street, New York, NY', + ), + _SearchHistoryTile( + search: 'Sushi Nakazawa', + address: 'Commerce Street, New York, NY', + ), + _SearchHistoryTile( + search: 'IFC Center', + address: '6th Avenue, New York, NY', + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +class _SectionHeader extends StatelessWidget { + const _SectionHeader({required this.title}); + + final String title; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsetsDirectional.only( + start: 16, + top: 16, + bottom: 16, + ), + child: Text( + title, + style: Theme.of(context).textTheme.labelLarge, + ), + ); + } +} + +class _SearchHistoryTile extends StatelessWidget { + const _SearchHistoryTile({ + this.icon = Icons.access_time, + required this.search, + required this.address, + }); + + final IconData icon; + final String search; + final String address; + + @override + Widget build(BuildContext context) { + return ListTile( + leading: Icon(icon), + title: Text(search), + subtitle: Text(address), + onTap: () {}, + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/reply/waterfall_notched_rectangle.dart b/dev/integration_tests/new_gallery/lib/studies/reply/waterfall_notched_rectangle.dart new file mode 100644 index 0000000000..92b3214d48 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/reply/waterfall_notched_rectangle.dart @@ -0,0 +1,97 @@ +// 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 'dart:math' as math; + +import 'package:flutter/material.dart'; + +/// A rectangle with a smooth circular notch. +/// +/// See also: +/// +/// * [CircleBorder], a [ShapeBorder] that describes a circle. +class WaterfallNotchedRectangle extends NotchedShape { + /// Creates a [WaterfallNotchedRectangle]. + /// + /// The same object can be used to create multiple shapes. + const WaterfallNotchedRectangle(); + + /// Creates a [Path] that describes a rectangle with a smooth circular notch. + /// + /// `host` is the bounding box for the returned shape. Conceptually this is + /// the rectangle to which the notch will be applied. + /// + /// `guest` is the bounding box of a circle that the notch accommodates. All + /// points in the circle bounded by `guest` will be outside of the returned + /// path. + /// + /// The notch is curve that smoothly connects the host's top edge and + /// the guest circle. + @override + Path getOuterPath(Rect host, Rect? guest) { + if (guest == null || !host.overlaps(guest)) { + return Path()..addRect(host); + } + + // The guest's shape is a circle bounded by the guest rectangle. + // So the guest's radius is half the guest width. + final double notchRadius = guest.width / 2.0; + + // We build a path for the notch from 3 segments: + // Segment A - a Bezier curve from the host's top edge to segment B. + // Segment B - an arc with radius notchRadius. + // Segment C - a Bezier curve from segment B back to the host's top edge. + // + // A detailed explanation and the derivation of the formulas below is + // available at: https://goo.gl/Ufzrqn + + // s1, s2 are the two knobs controlling the behavior of the bezzier curve. + const double s1 = 21.0; + const double s2 = 6.0; + + final double r = notchRadius; + final double a = -1.0 * r - s2; + final double b = host.top - guest.center.dy; + + final double n2 = math.sqrt(b * b * r * r * (a * a + b * b - r * r)); + final double p2xA = ((a * r * r) - n2) / (a * a + b * b); + final double p2xB = ((a * r * r) + n2) / (a * a + b * b); + final double p2yA = math.sqrt(r * r - p2xA * p2xA); + final double p2yB = math.sqrt(r * r - p2xB * p2xB); + + final List p = List.filled(6, null); + + // p0, p1, and p2 are the control points for segment A. + p[0] = Offset(a - s1, b); + p[1] = Offset(a, b); + final double cmp = b < 0 ? -1.0 : 1.0; + p[2] = cmp * p2yA > cmp * p2yB ? Offset(p2xA, p2yA) : Offset(p2xB, p2yB); + + // p3, p4, and p5 are the control points for segment B, which is a mirror + // of segment A around the y axis. + p[3] = Offset(-1.0 * p[2]!.dx, p[2]!.dy); + p[4] = Offset(-1.0 * p[1]!.dx, p[1]!.dy); + p[5] = Offset(-1.0 * p[0]!.dx, p[0]!.dy); + + // translate all points back to the absolute coordinate system. + for (int i = 0; i < p.length; i += 1) { + p[i] = p[i]! + guest.center; + } + + return Path() + ..moveTo(host.left, host.top) + ..lineTo(p[0]!.dx, p[0]!.dy) + ..quadraticBezierTo(p[1]!.dx, p[1]!.dy, p[2]!.dx, p[2]!.dy) + ..arcToPoint( + p[3]!, + radius: Radius.circular(notchRadius), + clockwise: false, + ) + ..quadraticBezierTo(p[4]!.dx, p[4]!.dy, p[5]!.dx, p[5]!.dy) + ..lineTo(host.right, host.top) + ..lineTo(host.right, host.bottom) + ..lineTo(host.left, host.bottom) + ..close(); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/shrine/app.dart b/dev/integration_tests/new_gallery/lib/studies/shrine/app.dart new file mode 100644 index 0000000000..965cae14bc --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/shrine/app.dart @@ -0,0 +1,215 @@ +// 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:flutter/material.dart'; +import 'package:scoped_model/scoped_model.dart'; + +import '../../data/gallery_options.dart'; +import '../../gallery_localizations.dart'; +import '../../layout/adaptive.dart'; +import 'backdrop.dart'; +import 'category_menu_page.dart'; +import 'expanding_bottom_sheet.dart'; +import 'home.dart'; +import 'login.dart'; +import 'model/app_state_model.dart'; +import 'model/product.dart'; +import 'page_status.dart'; +import 'routes.dart' as routes; +import 'scrim.dart'; +import 'supplemental/layout_cache.dart'; +import 'theme.dart'; + +class ShrineApp extends StatefulWidget { + const ShrineApp({super.key}); + + static const String loginRoute = routes.loginRoute; + static const String homeRoute = routes.homeRoute; + + @override + State createState() => _ShrineAppState(); +} + +class _ShrineAppState extends State + 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>> _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((AnimationStatus 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((AnimationStatus 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 _onWillPop() async { + final AnimationStatus 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: (BuildContext context, BoxConstraints constraints) => HomePage( + backdrop: isDisplayDesktop(context) + ? desktopBackdrop() + : mobileBackdrop(), + scrim: Scrim(controller: _expandingController), + expandingBottomSheet: ExpandingBottomSheet( + hideController: _controller, + expandingController: _expandingController, + ), + ), + ), + ), + ); + + return ScopedModel( + model: _model.value, + // ignore: deprecated_member_use + 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: (BuildContext context) => const LoginPage(), + ShrineApp.homeRoute: (BuildContext 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 { + @override + AppStateModel createDefaultValue() => AppStateModel()..loadProducts(); + + @override + AppStateModel fromPrimitives(Object? data) { + final AppStateModel appState = AppStateModel()..loadProducts(); + final Map appData = Map.from(data! as Map); + + // Reset selected category. + final int categoryIndex = appData['category_index'] as int; + appState.setCategory(categories[categoryIndex]); + + // Reset cart items. + final Map cartItems = appData['cart_data'] as Map; + cartItems.forEach((dynamic id, dynamic quantity) { + appState.addMultipleProductsToCart(id as int, quantity as int); + }); + + return appState; + } + + @override + Object toPrimitives() { + return { + 'cart_data': value.productsInCart, + 'category_index': categories.indexOf(value.selectedCategory), + }; + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/shrine/backdrop.dart b/dev/integration_tests/new_gallery/lib/studies/shrine/backdrop.dart new file mode 100644 index 0000000000..4735de88ad --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/shrine/backdrop.dart @@ -0,0 +1,393 @@ +// 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 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import '../../gallery_localizations.dart'; +import 'category_menu_page.dart'; +import 'page_status.dart'; + +const Cubic _accelerateCurve = Cubic(0.548, 0, 0.757, 0.464); +const Cubic _decelerateCurve = Cubic(0.23, 0.94, 0.41, 1); +const double _peakVelocityTime = 0.248210; +const double _peakVelocityProgress = 0.379146; + +class _FrontLayer extends StatelessWidget { + const _FrontLayer({ + this.onTap, + required this.child, + }); + + final VoidCallback? onTap; + final Widget child; + + @override + Widget build(BuildContext context) { + // An area at the top of the product page. + // When the menu page is shown, tapping this area will close the menu + // page and reveal the product page. + final Widget pageTopArea = Container( + height: 40, + alignment: AlignmentDirectional.centerStart, + ); + + return Material( + elevation: 16, + shape: const BeveledRectangleBorder( + borderRadius: + BorderRadiusDirectional.only(topStart: Radius.circular(46)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (onTap != null) MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + excludeFromSemantics: + true, // Because there is already a "Close Menu" button on screen. + onTap: onTap, + child: pageTopArea, + ), + ) else pageTopArea, + Expanded( + child: child, + ), + ], + ), + ); + } +} + +class _BackdropTitle extends AnimatedWidget { + const _BackdropTitle({ + required Animation super.listenable, + this.onPress, + required this.frontTitle, + required this.backTitle, + }); + + final void Function()? onPress; + final Widget frontTitle; + final Widget backTitle; + + @override + Widget build(BuildContext context) { + final Animation animation = CurvedAnimation( + parent: listenable as Animation, + curve: const Interval(0, 0.78), + ); + + final int textDirectionScalar = + Directionality.of(context) == TextDirection.ltr ? 1 : -1; + + const ImageIcon slantedMenuIcon = + ImageIcon(AssetImage('packages/shrine_images/slanted_menu.png')); + + final Widget directionalSlantedMenuIcon = + Directionality.of(context) == TextDirection.ltr + ? slantedMenuIcon + : Transform( + alignment: Alignment.center, + transform: Matrix4.rotationY(pi), + child: slantedMenuIcon, + ); + + final String? menuButtonTooltip = animation.isCompleted + ? GalleryLocalizations.of(context)!.shrineTooltipOpenMenu + : animation.isDismissed + ? GalleryLocalizations.of(context)!.shrineTooltipCloseMenu + : null; + + return DefaultTextStyle( + style: Theme.of(context).primaryTextTheme.titleLarge!, + softWrap: false, + overflow: TextOverflow.ellipsis, + child: Row(children: [ + // branded icon + SizedBox( + width: 72, + child: Semantics( + container: true, + child: IconButton( + padding: const EdgeInsetsDirectional.only(end: 8), + onPressed: onPress, + tooltip: menuButtonTooltip, + icon: Stack(children: [ + Opacity( + opacity: animation.value, + child: directionalSlantedMenuIcon, + ), + FractionalTranslation( + translation: Tween( + begin: Offset.zero, + end: Offset(1.0 * textDirectionScalar, 0.0), + ).evaluate(animation), + child: const ImageIcon( + AssetImage('packages/shrine_images/diamond.png'), + ), + ), + ]), + ), + ), + ), + // Here, we do a custom cross fade between backTitle and frontTitle. + // This makes a smooth animation between the two texts. + Stack( + children: [ + Opacity( + opacity: CurvedAnimation( + parent: ReverseAnimation(animation), + curve: const Interval(0.5, 1), + ).value, + child: FractionalTranslation( + translation: Tween( + begin: Offset.zero, + end: Offset(0.5 * textDirectionScalar, 0), + ).evaluate(animation), + child: backTitle, + ), + ), + Opacity( + opacity: CurvedAnimation( + parent: animation, + curve: const Interval(0.5, 1), + ).value, + child: FractionalTranslation( + translation: Tween( + begin: Offset(-0.25 * textDirectionScalar, 0), + end: Offset.zero, + ).evaluate(animation), + child: frontTitle, + ), + ), + ], + ), + ]), + ); + } +} + +/// Builds a Backdrop. +/// +/// A Backdrop widget has two layers, front and back. The front layer is shown +/// by default, and slides down to show the back layer, from which a user +/// can make a selection. The user can also configure the titles for when the +/// front or back layer is showing. +class Backdrop extends StatefulWidget { + const Backdrop({ + super.key, + required this.frontLayer, + required this.backLayer, + required this.frontTitle, + required this.backTitle, + required this.controller, + }); + + final Widget frontLayer; + final Widget backLayer; + final Widget frontTitle; + final Widget backTitle; + final AnimationController controller; + + @override + State createState() => _BackdropState(); +} + +class _BackdropState extends State + with SingleTickerProviderStateMixin { + final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop'); + late AnimationController _controller; + late Animation _layerAnimation; + + @override + void initState() { + super.initState(); + _controller = widget.controller; + } + + bool get _frontLayerVisible { + final AnimationStatus status = _controller.status; + return status == AnimationStatus.completed || + status == AnimationStatus.forward; + } + + void _toggleBackdropLayerVisibility() { + // Call setState here to update layerAnimation if that's necessary + setState(() { + _frontLayerVisible ? _controller.reverse() : _controller.forward(); + }); + } + + // _layerAnimation animates the front layer between open and close. + // _getLayerAnimation adjusts the values in the TweenSequence so the + // curve and timing are correct in both directions. + Animation _getLayerAnimation(Size layerSize, double layerTop) { + Curve firstCurve; // Curve for first TweenSequenceItem + Curve secondCurve; // Curve for second TweenSequenceItem + double firstWeight; // Weight of first TweenSequenceItem + double secondWeight; // Weight of second TweenSequenceItem + Animation animation; // Animation on which TweenSequence runs + + if (_frontLayerVisible) { + firstCurve = _accelerateCurve; + secondCurve = _decelerateCurve; + firstWeight = _peakVelocityTime; + secondWeight = 1 - _peakVelocityTime; + animation = CurvedAnimation( + parent: _controller.view, + curve: const Interval(0, 0.78), + ); + } else { + // These values are only used when the controller runs from t=1.0 to t=0.0 + firstCurve = _decelerateCurve.flipped; + secondCurve = _accelerateCurve.flipped; + firstWeight = 1 - _peakVelocityTime; + secondWeight = _peakVelocityTime; + animation = _controller.view; + } + + return TweenSequence( + >[ + TweenSequenceItem( + tween: RelativeRectTween( + begin: RelativeRect.fromLTRB( + 0, + layerTop, + 0, + layerTop - layerSize.height, + ), + end: RelativeRect.fromLTRB( + 0, + layerTop * _peakVelocityProgress, + 0, + (layerTop - layerSize.height) * _peakVelocityProgress, + ), + ).chain(CurveTween(curve: firstCurve)), + weight: firstWeight, + ), + TweenSequenceItem( + tween: RelativeRectTween( + begin: RelativeRect.fromLTRB( + 0, + layerTop * _peakVelocityProgress, + 0, + (layerTop - layerSize.height) * _peakVelocityProgress, + ), + end: RelativeRect.fill, + ).chain(CurveTween(curve: secondCurve)), + weight: secondWeight, + ), + ], + ).animate(animation); + } + + Widget _buildStack(BuildContext context, BoxConstraints constraints) { + const int layerTitleHeight = 48; + final Size layerSize = constraints.biggest; + final double layerTop = layerSize.height - layerTitleHeight; + + _layerAnimation = _getLayerAnimation(layerSize, layerTop); + + return Stack( + key: _backdropKey, + children: [ + ExcludeSemantics( + excluding: _frontLayerVisible, + child: widget.backLayer, + ), + PositionedTransition( + rect: _layerAnimation, + child: ExcludeSemantics( + excluding: !_frontLayerVisible, + child: AnimatedBuilder( + animation: PageStatus.of(context)!.cartController, + builder: (BuildContext context, Widget? child) => AnimatedBuilder( + animation: PageStatus.of(context)!.menuController, + builder: (BuildContext context, Widget? child) => _FrontLayer( + onTap: menuPageIsVisible(context) + ? _toggleBackdropLayerVisibility + : null, + child: widget.frontLayer, + ), + ), + ), + ), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + final AppBar appBar = AppBar( + automaticallyImplyLeading: false, + systemOverlayStyle: SystemUiOverlayStyle.dark, + elevation: 0, + titleSpacing: 0, + title: _BackdropTitle( + listenable: _controller.view, + onPress: _toggleBackdropLayerVisibility, + frontTitle: widget.frontTitle, + backTitle: widget.backTitle, + ), + actions: [ + IconButton( + icon: const Icon(Icons.search), + tooltip: GalleryLocalizations.of(context)!.shrineTooltipSearch, + onPressed: () {}, + ), + IconButton( + icon: const Icon(Icons.tune), + tooltip: GalleryLocalizations.of(context)!.shrineTooltipSettings, + onPressed: () {}, + ), + ], + ); + return AnimatedBuilder( + animation: PageStatus.of(context)!.cartController, + builder: (BuildContext context, Widget? child) => ExcludeSemantics( + excluding: cartPageIsVisible(context), + child: Scaffold( + appBar: appBar, + body: LayoutBuilder( + builder: _buildStack, + ), + ), + ), + ); + } +} + +class DesktopBackdrop extends StatelessWidget { + const DesktopBackdrop({ + super.key, + required this.frontLayer, + required this.backLayer, + }); + + final Widget frontLayer; + final Widget backLayer; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + backLayer, + Padding( + padding: EdgeInsetsDirectional.only( + start: desktopCategoryMenuPageWidth(context: context), + ), + child: Material( + elevation: 16, + color: Colors.white, + child: frontLayer, + ), + ) + ], + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/shrine/category_menu_page.dart b/dev/integration_tests/new_gallery/lib/studies/shrine/category_menu_page.dart new file mode 100644 index 0000000000..ad2f37aed5 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/shrine/category_menu_page.dart @@ -0,0 +1,216 @@ +// 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:flutter/material.dart'; +import 'package:scoped_model/scoped_model.dart'; + +import '../../data/gallery_options.dart'; +import '../../gallery_localizations.dart'; +import '../../layout/adaptive.dart'; +import '../../layout/text_scale.dart'; +import 'app.dart'; +import 'colors.dart'; +import 'model/app_state_model.dart'; +import 'model/product.dart'; +import 'page_status.dart'; +import 'triangle_category_indicator.dart'; + +double desktopCategoryMenuPageWidth({ + required BuildContext context, +}) { + return 232 * reducedTextScale(context); +} + +class CategoryMenuPage extends StatelessWidget { + const CategoryMenuPage({ + super.key, + this.onCategoryTap, + }); + + final VoidCallback? onCategoryTap; + + Widget _buttonText(String caption, TextStyle style) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Text( + caption, + style: style, + textAlign: TextAlign.center, + ), + ); + } + + Widget _divider({required BuildContext context}) { + return Container( + width: 56 * GalleryOptions.of(context).textScaleFactor(context), + height: 1, + color: const Color(0xFF8F716D), + ); + } + + Widget _buildCategory(Category category, BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + + final String categoryString = category.name(context); + + final TextStyle selectedCategoryTextStyle = Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(fontSize: isDesktop ? 17 : 19); + + final TextStyle unselectedCategoryTextStyle = selectedCategoryTextStyle.copyWith( + color: shrineBrown900.withOpacity(0.6)); + + final double indicatorHeight = (isDesktop ? 28 : 30) * + GalleryOptions.of(context).textScaleFactor(context); + final double indicatorWidth = indicatorHeight * 34 / 28; + + return ScopedModelDescendant( + builder: (BuildContext context, Widget? child, AppStateModel model) => Semantics( + selected: model.selectedCategory == category, + button: true, + enabled: true, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + model.setCategory(category); + if (onCategoryTap != null) { + onCategoryTap!(); + } + }, + child: model.selectedCategory == category + ? CustomPaint( + painter: TriangleCategoryIndicator( + indicatorWidth, + indicatorHeight, + ), + child: + _buttonText(categoryString, selectedCategoryTextStyle), + ) + : _buttonText(categoryString, unselectedCategoryTextStyle), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + + final TextStyle logoutTextStyle = Theme.of(context).textTheme.bodyLarge!.copyWith( + fontSize: isDesktop ? 17 : 19, + color: shrineBrown900.withOpacity(0.6), + ); + + if (isDesktop) { + return AnimatedBuilder( + animation: PageStatus.of(context)!.cartController, + builder: (BuildContext context, Widget? child) => ExcludeSemantics( + excluding: !menuPageIsVisible(context), + child: Material( + child: Container( + color: shrinePink100, + width: desktopCategoryMenuPageWidth(context: context), + child: Column( + children: [ + const SizedBox(height: 64), + Image.asset( + 'packages/shrine_images/diamond.png', + excludeFromSemantics: true, + ), + const SizedBox(height: 16), + Semantics( + container: true, + child: Text( + 'SHRINE', + style: Theme.of(context).textTheme.headlineSmall, + ), + ), + const Spacer(), + for (final Category category in categories) + _buildCategory(category, context), + _divider(context: context), + Semantics( + button: true, + enabled: true, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + Navigator.of(context) + .restorablePushNamed(ShrineApp.loginRoute); + }, + child: _buttonText( + GalleryLocalizations.of(context)! + .shrineLogoutButtonCaption, + logoutTextStyle, + ), + ), + ), + ), + const Spacer(), + IconButton( + icon: const Icon(Icons.search), + tooltip: + GalleryLocalizations.of(context)!.shrineTooltipSearch, + onPressed: () {}, + ), + const SizedBox(height: 72), + ], + ), + ), + ), + ), + ); + } else { + return AnimatedBuilder( + animation: PageStatus.of(context)!.cartController, + builder: (BuildContext context, Widget? child) => AnimatedBuilder( + animation: PageStatus.of(context)!.menuController, + builder: (BuildContext context, Widget? child) => ExcludeSemantics( + excluding: !menuPageIsVisible(context), + child: Center( + child: Container( + padding: const EdgeInsets.only(top: 40), + color: shrinePink100, + child: ListView( + children: [ + for (final Category category in categories) + _buildCategory(category, context), + Center( + child: _divider(context: context), + ), + Semantics( + button: true, + enabled: true, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + if (onCategoryTap != null) { + onCategoryTap!(); + } + Navigator.of(context) + .restorablePushNamed(ShrineApp.loginRoute); + }, + child: _buttonText( + GalleryLocalizations.of(context)! + .shrineLogoutButtonCaption, + logoutTextStyle, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/shrine/colors.dart b/dev/integration_tests/new_gallery/lib/studies/shrine/colors.dart new file mode 100644 index 0000000000..caea6bb088 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/shrine/colors.dart @@ -0,0 +1,18 @@ +// 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:flutter/material.dart'; + +const Color shrinePink50 = Color(0xFFFEEAE6); +const Color shrinePink100 = Color(0xFFFEDBD0); +const Color shrinePink300 = Color(0xFFFBB8AC); +const Color shrinePink400 = Color(0xFFEAA4A4); + +const Color shrineBrown900 = Color(0xFF442B2D); +const Color shrineBrown600 = Color(0xFF7D4F52); + +const Color shrineErrorRed = Color(0xFFC5032B); + +const Color shrineSurfaceWhite = Color(0xFFFFFBFA); +const Color shrineBackgroundWhite = Colors.white; diff --git a/dev/integration_tests/new_gallery/lib/studies/shrine/expanding_bottom_sheet.dart b/dev/integration_tests/new_gallery/lib/studies/shrine/expanding_bottom_sheet.dart new file mode 100644 index 0000000000..51e062f66d --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/shrine/expanding_bottom_sheet.dart @@ -0,0 +1,815 @@ +// 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 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:scoped_model/scoped_model.dart'; + +import '../../gallery_localizations.dart'; +import '../../layout/adaptive.dart'; +import '../../layout/text_scale.dart'; +import 'colors.dart'; +import 'model/app_state_model.dart'; +import 'model/product.dart'; +import 'page_status.dart'; +import 'shopping_cart.dart'; + +// These curves define the emphasized easing curve. +const Cubic _accelerateCurve = Cubic(0.548, 0, 0.757, 0.464); +const Cubic _decelerateCurve = Cubic(0.23, 0.94, 0.41, 1); +// The time at which the accelerate and decelerate curves switch off +const double _peakVelocityTime = 0.248210; +// Percent (as a decimal) of animation that should be completed at _peakVelocityTime +const double _peakVelocityProgress = 0.379146; +// Radius of the shape on the top start of the sheet for mobile layouts. +const double _mobileCornerRadius = 24.0; +// Radius of the shape on the top start and bottom start of the sheet for mobile layouts. +const double _desktopCornerRadius = 12.0; +// Width for just the cart icon and no thumbnails. +const double _cartIconWidth = 64.0; +// Height for just the cart icon and no thumbnails. +const double _cartIconHeight = 56.0; +// Height of a thumbnail. +const double _defaultThumbnailHeight = 40.0; +// Gap between thumbnails. +const double _thumbnailGap = 16.0; + +// Maximum number of thumbnails shown in the cart. +const int _maxThumbnailCount = 3; + +double _thumbnailHeight(BuildContext context) { + return _defaultThumbnailHeight * reducedTextScale(context); +} + +double _paddedThumbnailHeight(BuildContext context) { + return _thumbnailHeight(context) + _thumbnailGap; +} + +class ExpandingBottomSheet extends StatefulWidget { + const ExpandingBottomSheet({ + super.key, + required this.hideController, + required this.expandingController, + }); + + final AnimationController hideController; + final AnimationController expandingController; + + @override + ExpandingBottomSheetState createState() => ExpandingBottomSheetState(); + + static ExpandingBottomSheetState? of(BuildContext context, + {bool isNullOk = false}) { + final ExpandingBottomSheetState? result = context.findAncestorStateOfType(); + if (isNullOk || result != null) { + return result; + } + throw FlutterError( + 'ExpandingBottomSheet.of() called with a context that does not contain a ExpandingBottomSheet.\n'); + } +} + +// Emphasized Easing is a motion curve that has an organic, exciting feeling. +// It's very fast to begin with and then very slow to finish. Unlike standard +// curves, like [Curves.fastOutSlowIn], it can't be expressed in a cubic bezier +// curve formula. It's quintic, not cubic. But it _can_ be expressed as one +// curve followed by another, which we do here. +Animation _getEmphasizedEasingAnimation({ + required T begin, + required T peak, + required T end, + required bool isForward, + required Animation parent, +}) { + Curve firstCurve; + Curve secondCurve; + double firstWeight; + double secondWeight; + + if (isForward) { + firstCurve = _accelerateCurve; + secondCurve = _decelerateCurve; + firstWeight = _peakVelocityTime; + secondWeight = 1 - _peakVelocityTime; + } else { + firstCurve = _decelerateCurve.flipped; + secondCurve = _accelerateCurve.flipped; + firstWeight = 1 - _peakVelocityTime; + secondWeight = _peakVelocityTime; + } + + return TweenSequence( + >[ + TweenSequenceItem( + weight: firstWeight, + tween: Tween( + begin: begin, + end: peak, + ).chain(CurveTween(curve: firstCurve)), + ), + TweenSequenceItem( + weight: secondWeight, + tween: Tween( + begin: peak, + end: end, + ).chain(CurveTween(curve: secondCurve)), + ), + ], + ).animate(parent); +} + +// Calculates the value where two double Animations should be joined. Used by +// callers of _getEmphasisedEasing(). +double _getPeakPoint({required double begin, required double end}) { + return begin + (end - begin) * _peakVelocityProgress; +} + +class ExpandingBottomSheetState extends State { + final GlobalKey _expandingBottomSheetKey = + GlobalKey(debugLabel: 'Expanding bottom sheet'); + + // The width of the Material, calculated by _widthFor() & based on the number + // of products in the cart. 64.0 is the width when there are 0 products + // (_kWidthForZeroProducts) + double _width = _cartIconWidth; + double _height = _cartIconHeight; + + // Controller for the opening and closing of the ExpandingBottomSheet + AnimationController get _controller => widget.expandingController; + + // Animations for the opening and closing of the ExpandingBottomSheet + late Animation _widthAnimation; + late Animation _heightAnimation; + late Animation _thumbnailOpacityAnimation; + late Animation _cartOpacityAnimation; + late Animation _topStartShapeAnimation; + late Animation _bottomStartShapeAnimation; + late Animation _slideAnimation; + late Animation _gapAnimation; + + Animation _getWidthAnimation(double screenWidth) { + if (_controller.status == AnimationStatus.forward) { + // Opening animation + return Tween(begin: _width, end: screenWidth).animate( + CurvedAnimation( + parent: _controller.view, + curve: const Interval(0, 0.3, curve: Curves.fastOutSlowIn), + ), + ); + } else { + // Closing animation + return _getEmphasizedEasingAnimation( + begin: _width, + peak: _getPeakPoint(begin: _width, end: screenWidth), + end: screenWidth, + isForward: false, + parent: CurvedAnimation( + parent: _controller.view, curve: const Interval(0, 0.87)), + ); + } + } + + Animation _getHeightAnimation(double screenHeight) { + if (_controller.status == AnimationStatus.forward) { + // Opening animation + + return _getEmphasizedEasingAnimation( + begin: _height, + peak: _getPeakPoint(begin: _height, end: screenHeight), + end: screenHeight, + isForward: true, + parent: _controller.view, + ); + } else { + // Closing animation + return Tween( + begin: _height, + end: screenHeight, + ).animate( + CurvedAnimation( + parent: _controller.view, + curve: const Interval(0.434, 1), // not used + // only the reverseCurve will be used + reverseCurve: Interval(0.434, 1, curve: Curves.fastOutSlowIn.flipped), + ), + ); + } + } + + Animation _getDesktopGapAnimation(double gapHeight) { + final double collapsedGapHeight = gapHeight; + const double expandedGapHeight = 0.0; + + if (_controller.status == AnimationStatus.forward) { + // Opening animation + + return _getEmphasizedEasingAnimation( + begin: collapsedGapHeight, + peak: collapsedGapHeight + + (expandedGapHeight - collapsedGapHeight) * _peakVelocityProgress, + end: expandedGapHeight, + isForward: true, + parent: _controller.view, + ); + } else { + // Closing animation + return Tween( + begin: collapsedGapHeight, + end: expandedGapHeight, + ).animate( + CurvedAnimation( + parent: _controller.view, + curve: const Interval(0.434, 1), // not used + // only the reverseCurve will be used + reverseCurve: Interval(0.434, 1, curve: Curves.fastOutSlowIn.flipped), + ), + ); + } + } + + // Animation of the top-start cut corner. It's cut when closed and not cut when open. + Animation _getShapeTopStartAnimation(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + + final double cornerRadius = isDesktop ? _desktopCornerRadius : _mobileCornerRadius; + + if (_controller.status == AnimationStatus.forward) { + return Tween(begin: cornerRadius, end: 0).animate( + CurvedAnimation( + parent: _controller.view, + curve: const Interval(0, 0.3, curve: Curves.fastOutSlowIn), + ), + ); + } else { + return _getEmphasizedEasingAnimation( + begin: cornerRadius, + peak: _getPeakPoint(begin: cornerRadius, end: 0), + end: 0, + isForward: false, + parent: _controller.view, + ); + } + } + + // Animation of the bottom-start cut corner. It's cut when closed and not cut when open. + Animation _getShapeBottomStartAnimation(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + + final double cornerRadius = isDesktop ? _desktopCornerRadius : 0.0; + + if (_controller.status == AnimationStatus.forward) { + return Tween(begin: cornerRadius, end: 0).animate( + CurvedAnimation( + parent: _controller.view, + curve: const Interval(0, 0.3, curve: Curves.fastOutSlowIn), + ), + ); + } else { + return _getEmphasizedEasingAnimation( + begin: cornerRadius, + peak: _getPeakPoint(begin: cornerRadius, end: 0), + end: 0, + isForward: false, + parent: _controller.view, + ); + } + } + + Animation _getThumbnailOpacityAnimation() { + return Tween(begin: 1, end: 0).animate( + CurvedAnimation( + parent: _controller.view, + curve: _controller.status == AnimationStatus.forward + ? const Interval(0, 0.3) + : const Interval(0.532, 0.766), + ), + ); + } + + Animation _getCartOpacityAnimation() { + return CurvedAnimation( + parent: _controller.view, + curve: _controller.status == AnimationStatus.forward + ? const Interval(0.3, 0.6) + : const Interval(0.766, 1), + ); + } + + // Returns the correct width of the ExpandingBottomSheet based on the number of + // products and the text scaling options in the cart in the mobile layout. + double _mobileWidthFor(int numProducts, BuildContext context) { + final int cartThumbnailGap = numProducts > 0 ? 16 : 0; + final double thumbnailsWidth = + min(numProducts, _maxThumbnailCount) * _paddedThumbnailHeight(context); + final num overflowNumberWidth = + numProducts > _maxThumbnailCount ? 30 * cappedTextScale(context) : 0; + return _cartIconWidth + + cartThumbnailGap + + thumbnailsWidth + + overflowNumberWidth; + } + + // Returns the correct height of the ExpandingBottomSheet based on the text scaling + // options in the mobile layout. + double _mobileHeightFor(BuildContext context) { + return _paddedThumbnailHeight(context); + } + + // Returns the correct width of the ExpandingBottomSheet based on the text scaling + // options in the desktop layout. + double _desktopWidthFor(BuildContext context) { + return _paddedThumbnailHeight(context) + 8; + } + + // Returns the correct height of the ExpandingBottomSheet based on the number of + // products and the text scaling options in the cart in the desktop layout. + double _desktopHeightFor(int numProducts, BuildContext context) { + final int cartThumbnailGap = numProducts > 0 ? 8 : 0; + final double thumbnailsHeight = + min(numProducts, _maxThumbnailCount) * _paddedThumbnailHeight(context); + final num overflowNumberHeight = + numProducts > _maxThumbnailCount ? 28 * reducedTextScale(context) : 0; + return _cartIconHeight + + cartThumbnailGap + + thumbnailsHeight + + overflowNumberHeight; + } + + // Returns true if the cart is open or opening and false otherwise. + bool get _isOpen { + final AnimationStatus status = _controller.status; + return status == AnimationStatus.completed || + status == AnimationStatus.forward; + } + + // Opens the ExpandingBottomSheet if it's closed, otherwise does nothing. + void open() { + if (!_isOpen) { + _controller.forward(); + } + } + + // Closes the ExpandingBottomSheet if it's open or opening, otherwise does nothing. + void close() { + if (_isOpen) { + _controller.reverse(); + } + } + + // Changes the padding between the start edge of the Material and the cart icon + // based on the number of products in the cart (padding increases when > 0 + // products.) + EdgeInsetsDirectional _horizontalCartPaddingFor(int numProducts) { + return (numProducts == 0) + ? const EdgeInsetsDirectional.only(start: 20, end: 8) + : const EdgeInsetsDirectional.only(start: 32, end: 8); + } + + // Changes the padding above and below the cart icon + // based on the number of products in the cart (padding increases when > 0 + // products.) + EdgeInsets _verticalCartPaddingFor(int numProducts) { + return (numProducts == 0) + ? const EdgeInsets.only(top: 16, bottom: 16) + : const EdgeInsets.only(top: 16, bottom: 24); + } + + bool get _cartIsVisible => _thumbnailOpacityAnimation.value == 0; + + // We take 16 pts off of the bottom padding to ensure the collapsed shopping + // cart is not too tall. + double get _bottomSafeArea { + return max(MediaQuery.of(context).viewPadding.bottom - 16, 0); + } + + Widget _buildThumbnails(BuildContext context, int numProducts) { + final bool isDesktop = isDisplayDesktop(context); + + Widget thumbnails; + + if (isDesktop) { + thumbnails = Column( + children: [ + AnimatedPadding( + padding: _verticalCartPaddingFor(numProducts), + duration: const Duration(milliseconds: 225), + child: const Icon(Icons.shopping_cart), + ), + SizedBox( + width: _width, + height: min(numProducts, _maxThumbnailCount) * + _paddedThumbnailHeight(context), + child: const ProductThumbnailRow(), + ), + const ExtraProductsNumber(), + ], + ); + } else { + thumbnails = Column( + children: [ + Row( + children: [ + AnimatedPadding( + padding: _horizontalCartPaddingFor(numProducts), + duration: const Duration(milliseconds: 225), + child: const Icon(Icons.shopping_cart), + ), + Container( + // Accounts for the overflow number + width: min(numProducts, _maxThumbnailCount) * + _paddedThumbnailHeight(context) + + (numProducts > 0 ? _thumbnailGap : 0), + height: _height - _bottomSafeArea, + padding: const EdgeInsets.symmetric(vertical: 8), + child: const ProductThumbnailRow(), + ), + const ExtraProductsNumber(), + ], + ), + ], + ); + } + + return ExcludeSemantics( + child: Opacity( + opacity: _thumbnailOpacityAnimation.value, + child: thumbnails, + ), + ); + } + + Widget _buildShoppingCartPage() { + return Opacity( + opacity: _cartOpacityAnimation.value, + child: const ShoppingCartPage(), + ); + } + + Widget _buildCart(BuildContext context) { + // numProducts is the number of different products in the cart (does not + // include multiples of the same product). + final bool isDesktop = isDisplayDesktop(context); + + final AppStateModel model = ScopedModel.of(context); + final int numProducts = model.productsInCart.keys.length; + final int totalCartQuantity = model.totalCartQuantity; + final Size screenSize = MediaQuery.of(context).size; + final double screenWidth = screenSize.width; + final double screenHeight = screenSize.height; + + final double expandedCartWidth = isDesktop + ? (360 * cappedTextScale(context)).clamp(360, screenWidth).toDouble() + : screenWidth; + + _width = isDesktop + ? _desktopWidthFor(context) + : _mobileWidthFor(numProducts, context); + _widthAnimation = _getWidthAnimation(expandedCartWidth); + _height = isDesktop + ? _desktopHeightFor(numProducts, context) + : _mobileHeightFor(context) + _bottomSafeArea; + _heightAnimation = _getHeightAnimation(screenHeight); + _topStartShapeAnimation = _getShapeTopStartAnimation(context); + _bottomStartShapeAnimation = _getShapeBottomStartAnimation(context); + _thumbnailOpacityAnimation = _getThumbnailOpacityAnimation(); + _cartOpacityAnimation = _getCartOpacityAnimation(); + _gapAnimation = isDesktop + ? _getDesktopGapAnimation(116) + : const AlwaysStoppedAnimation(0); + + final Widget child = SizedBox( + width: _widthAnimation.value, + height: _heightAnimation.value, + child: Material( + animationDuration: Duration.zero, + shape: BeveledRectangleBorder( + borderRadius: BorderRadiusDirectional.only( + topStart: Radius.circular(_topStartShapeAnimation.value), + bottomStart: Radius.circular(_bottomStartShapeAnimation.value), + ), + ), + elevation: 4, + color: shrinePink50, + child: _cartIsVisible + ? _buildShoppingCartPage() + : _buildThumbnails(context, numProducts), + ), + ); + + final Widget childWithInteraction = productPageIsVisible(context) + ? Semantics( + button: true, + enabled: true, + label: GalleryLocalizations.of(context)! + .shrineScreenReaderCart(totalCartQuantity), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: open, + child: child, + ), + ), + ) + : child; + + return Padding( + padding: EdgeInsets.only(top: _gapAnimation.value), + child: childWithInteraction, + ); + } + + // Builder for the hide and reveal animation when the backdrop opens and closes + Widget _buildSlideAnimation(BuildContext context, Widget child) { + final bool isDesktop = isDisplayDesktop(context); + + if (isDesktop) { + return child; + } else { + final int textDirectionScalar = + Directionality.of(context) == TextDirection.ltr ? 1 : -1; + + _slideAnimation = _getEmphasizedEasingAnimation( + begin: Offset(1.0 * textDirectionScalar, 0.0), + peak: Offset(_peakVelocityProgress * textDirectionScalar, 0), + end: Offset.zero, + isForward: widget.hideController.status == AnimationStatus.forward, + parent: widget.hideController, + ); + + return SlideTransition( + position: _slideAnimation, + child: child, + ); + } + } + + @override + Widget build(BuildContext context) { + return AnimatedSize( + key: _expandingBottomSheetKey, + duration: const Duration(milliseconds: 225), + curve: Curves.easeInOut, + alignment: AlignmentDirectional.topStart, + child: AnimatedBuilder( + animation: widget.hideController, + builder: (BuildContext context, Widget? child) => AnimatedBuilder( + animation: widget.expandingController, + builder: (BuildContext context, Widget? child) => ScopedModelDescendant( + builder: (BuildContext context, Widget? child, AppStateModel model) => + _buildSlideAnimation(context, _buildCart(context)), + ), + ), + ), + ); + } +} + +class ProductThumbnailRow extends StatefulWidget { + const ProductThumbnailRow({super.key}); + + @override + State createState() => _ProductThumbnailRowState(); +} + +class _ProductThumbnailRowState extends State { + final GlobalKey _listKey = GlobalKey(); + + // _list represents what's currently on screen. If _internalList updates, + // it will need to be updated to match it. + late _ListModel _list; + + // _internalList represents the list as it is updated by the AppStateModel. + late List _internalList; + + @override + void initState() { + super.initState(); + _list = _ListModel( + listKey: _listKey, + initialItems: + ScopedModel.of(context).productsInCart.keys.toList(), + removedItemBuilder: _buildRemovedThumbnail, + ); + _internalList = List.from(_list.list); + } + + Product _productWithId(int productId) { + final AppStateModel model = ScopedModel.of(context); + final Product product = model.getProductById(productId); + return product; + } + + Widget _buildRemovedThumbnail( + int item, BuildContext context, Animation animation) { + return ProductThumbnail(animation, animation, _productWithId(item)); + } + + Widget _buildThumbnail( + BuildContext context, int index, Animation animation) { + final Animation thumbnailSize = Tween(begin: 0.8, end: 1).animate( + CurvedAnimation( + curve: const Interval(0.33, 1, curve: Curves.easeIn), + parent: animation, + ), + ); + + final Animation opacity = CurvedAnimation( + curve: const Interval(0.33, 1), + parent: animation, + ); + + return ProductThumbnail( + thumbnailSize, opacity, _productWithId(_list[index])); + } + + // If the lists are the same length, assume nothing has changed. + // If the internalList is shorter than the ListModel, an item has been removed. + // If the internalList is longer, then an item has been added. + void _updateLists() { + // Update _internalList based on the model + _internalList = + ScopedModel.of(context).productsInCart.keys.toList(); + final Set internalSet = Set.from(_internalList); + final Set listSet = Set.from(_list.list); + + final Set difference = internalSet.difference(listSet); + if (difference.isEmpty) { + return; + } + + for (final int product in difference) { + if (_internalList.length < _list.length) { + _list.remove(product); + } else if (_internalList.length > _list.length) { + _list.add(product); + } + } + + while (_internalList.length != _list.length) { + int index = 0; + // Check bounds and that the list elements are the same + while (_internalList.isNotEmpty && + _list.length > 0 && + index < _internalList.length && + index < _list.length && + _internalList[index] == _list[index]) { + index++; + } + } + } + + Widget _buildAnimatedList(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + + return AnimatedList( + key: _listKey, + shrinkWrap: true, + itemBuilder: _buildThumbnail, + initialItemCount: _list.length, + scrollDirection: isDesktop ? Axis.vertical : Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), // Cart shouldn't scroll + ); + } + + @override + Widget build(BuildContext context) { + return ScopedModelDescendant( + builder: (BuildContext context, Widget? child, AppStateModel model) { + _updateLists(); + return _buildAnimatedList(context); + }, + ); + } +} + +class ExtraProductsNumber extends StatelessWidget { + const ExtraProductsNumber({super.key}); + + // Calculates the number to be displayed at the end of the row if there are + // more than three products in the cart. This calculates overflow products, + // including their duplicates (but not duplicates of products shown as + // thumbnails). + int _calculateOverflow(AppStateModel model) { + final Map productMap = model.productsInCart; + // List created to be able to access products by index instead of ID. + // Order is guaranteed because productsInCart returns a LinkedHashMap. + final List products = productMap.keys.toList(); + int overflow = 0; + final int numProducts = products.length; + for (int i = _maxThumbnailCount; i < numProducts; i++) { + overflow += productMap[products[i]]!; + } + return overflow; + } + + Widget _buildOverflow(AppStateModel model, BuildContext context) { + if (model.productsInCart.length <= _maxThumbnailCount) { + return Container(); + } + + final int numOverflowProducts = _calculateOverflow(model); + // Maximum of 99 so padding doesn't get messy. + final int displayedOverflowProducts = + numOverflowProducts <= 99 ? numOverflowProducts : 99; + return Text( + '+$displayedOverflowProducts', + style: Theme.of(context).primaryTextTheme.labelLarge, + ); + } + + @override + Widget build(BuildContext context) { + return ScopedModelDescendant( + builder: (BuildContext builder, Widget? child, AppStateModel model) => _buildOverflow(model, context), + ); + } +} + +class ProductThumbnail extends StatelessWidget { + const ProductThumbnail(this.animation, this.opacityAnimation, this.product, + {super.key}); + + final Animation animation; + final Animation opacityAnimation; + final Product product; + + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + + return FadeTransition( + opacity: opacityAnimation, + child: ScaleTransition( + scale: animation, + child: Container( + width: _thumbnailHeight(context), + height: _thumbnailHeight(context), + decoration: BoxDecoration( + image: DecorationImage( + image: ExactAssetImage( + product.assetName, // asset name + package: product.assetPackage, // asset package + ), + fit: BoxFit.cover, + ), + borderRadius: const BorderRadius.all(Radius.circular(10)), + ), + margin: isDesktop + ? const EdgeInsetsDirectional.only(start: 12, end: 12, bottom: 16) + : const EdgeInsetsDirectional.only(start: 16), + ), + ), + ); + } +} + +// _ListModel manipulates an internal list and an AnimatedList +class _ListModel { + _ListModel({ + required this.listKey, + required this.removedItemBuilder, + Iterable? initialItems, + }) : _items = initialItems?.toList() ?? []; + + final GlobalKey listKey; + final Widget Function(int, BuildContext, Animation) + removedItemBuilder; + final List _items; + + AnimatedListState? get _animatedList => listKey.currentState; + + void add(int product) { + _insert(_items.length, product); + } + + void _insert(int index, int item) { + _items.insert(index, item); + _animatedList! + .insertItem(index, duration: const Duration(milliseconds: 225)); + } + + void remove(int product) { + final int index = _items.indexOf(product); + if (index >= 0) { + _removeAt(index); + } + } + + void _removeAt(int index) { + final int removedItem = _items.removeAt(index); + _animatedList!.removeItem(index, (BuildContext context, Animation animation) { + return removedItemBuilder(removedItem, context, animation); + }); + } + + int get length => _items.length; + + int operator [](int index) => _items[index]; + + int indexOf(int item) => _items.indexOf(item); + + List get list => _items; +} diff --git a/dev/integration_tests/new_gallery/lib/studies/shrine/home.dart b/dev/integration_tests/new_gallery/lib/studies/shrine/home.dart new file mode 100644 index 0000000000..c207c1153a --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/shrine/home.dart @@ -0,0 +1,75 @@ +// 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:flutter/material.dart'; +import 'package:flutter/semantics.dart'; +import 'package:scoped_model/scoped_model.dart'; + +import '../../data/gallery_options.dart'; +import '../../layout/adaptive.dart'; +import 'expanding_bottom_sheet.dart'; +import 'model/app_state_model.dart'; +import 'supplemental/asymmetric_view.dart'; + +const String _ordinalSortKeyName = 'home'; + +class ProductPage extends StatelessWidget { + const ProductPage({super.key}); + + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + + return ScopedModelDescendant( + builder: (BuildContext context, Widget? child, AppStateModel model) { + return isDesktop + ? DesktopAsymmetricView(products: model.getProducts()) + : MobileAsymmetricView(products: model.getProducts()); + }); + } +} + +class HomePage extends StatelessWidget { + const HomePage({ + this.expandingBottomSheet, + this.scrim, + this.backdrop, + super.key, + }); + + final ExpandingBottomSheet? expandingBottomSheet; + final Widget? scrim; + final Widget? backdrop; + + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + + // Use sort keys to make sure the cart button is always on the top. + // This way, a11y users do not have to scroll through the entire list to + // find the cart, and can easily get to the cart from anywhere on the page. + return ApplyTextOptions( + child: Stack( + children: [ + Semantics( + container: true, + sortKey: const OrdinalSortKey(1, name: _ordinalSortKeyName), + child: backdrop, + ), + ExcludeSemantics(child: scrim), + Align( + alignment: isDesktop + ? AlignmentDirectional.topEnd + : AlignmentDirectional.bottomEnd, + child: Semantics( + container: true, + sortKey: const OrdinalSortKey(0, name: _ordinalSortKeyName), + child: expandingBottomSheet, + ), + ), + ], + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/shrine/login.dart b/dev/integration_tests/new_gallery/lib/studies/shrine/login.dart new file mode 100644 index 0000000000..09ab376082 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/shrine/login.dart @@ -0,0 +1,215 @@ +// 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 'dart:math'; + +import 'package:flutter/material.dart'; + +import '../../data/gallery_options.dart'; +import '../../gallery_localizations.dart'; +import '../../layout/adaptive.dart'; +import '../../layout/image_placeholder.dart'; +import '../../layout/letter_spacing.dart'; +import '../../layout/text_scale.dart'; +import 'app.dart'; +import 'theme.dart'; + +const double _horizontalPadding = 24.0; + +double desktopLoginScreenMainAreaWidth({required BuildContext context}) { + return min( + 360 * reducedTextScale(context), + MediaQuery.of(context).size.width - 2 * _horizontalPadding, + ); +} + +class LoginPage extends StatelessWidget { + const LoginPage({super.key}); + + @override + Widget build(BuildContext context) { + final bool isDesktop = isDisplayDesktop(context); + + return ApplyTextOptions( + child: isDesktop + ? LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) => Scaffold( + body: SafeArea( + child: Center( + child: SizedBox( + width: desktopLoginScreenMainAreaWidth(context: context), + child: const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _ShrineLogo(), + SizedBox(height: 40), + _UsernameTextField(), + SizedBox(height: 16), + _PasswordTextField(), + SizedBox(height: 24), + _CancelAndNextButtons(), + SizedBox(height: 62), + ], + ), + ), + ), + ), + ), + ) + : Scaffold( + body: SafeArea( + child: ListView( + restorationId: 'login_list_view', + physics: const ClampingScrollPhysics(), + padding: const EdgeInsets.symmetric( + horizontal: _horizontalPadding, + ), + children: const [ + SizedBox(height: 80), + _ShrineLogo(), + SizedBox(height: 120), + _UsernameTextField(), + SizedBox(height: 12), + _PasswordTextField(), + _CancelAndNextButtons(), + ], + ), + ), + ), + ); + } +} + +class _ShrineLogo extends StatelessWidget { + const _ShrineLogo(); + + @override + Widget build(BuildContext context) { + return ExcludeSemantics( + child: Column( + children: [ + const FadeInImagePlaceholder( + image: AssetImage('packages/shrine_images/diamond.png'), + placeholder: SizedBox( + width: 34, + height: 34, + ), + ), + const SizedBox(height: 16), + Text( + 'SHRINE', + style: Theme.of(context).textTheme.headlineSmall, + ), + ], + ), + ); + } +} + +class _UsernameTextField extends StatelessWidget { + const _UsernameTextField(); + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + + return TextField( + textInputAction: TextInputAction.next, + restorationId: 'username_text_field', + cursorColor: colorScheme.onSurface, + decoration: InputDecoration( + labelText: GalleryLocalizations.of(context)!.shrineLoginUsernameLabel, + labelStyle: TextStyle( + letterSpacing: letterSpacingOrNone(mediumLetterSpacing), + ), + ), + ); + } +} + +class _PasswordTextField extends StatelessWidget { + const _PasswordTextField(); + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + + return TextField( + restorationId: 'password_text_field', + cursorColor: colorScheme.onSurface, + obscureText: true, + decoration: InputDecoration( + labelText: GalleryLocalizations.of(context)!.shrineLoginPasswordLabel, + labelStyle: TextStyle( + letterSpacing: letterSpacingOrNone(mediumLetterSpacing), + ), + ), + ); + } +} + +class _CancelAndNextButtons extends StatelessWidget { + const _CancelAndNextButtons(); + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + + final bool isDesktop = isDisplayDesktop(context); + + final EdgeInsets buttonTextPadding = isDesktop + ? const EdgeInsets.symmetric(horizontal: 24, vertical: 16) + : EdgeInsets.zero; + + return Padding( + padding: isDesktop ? EdgeInsets.zero : const EdgeInsets.all(8), + child: OverflowBar( + spacing: isDesktop ? 0 : 8, + alignment: MainAxisAlignment.end, + children: [ + TextButton( + style: TextButton.styleFrom( + shape: const BeveledRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(7)), + ), + ), + onPressed: () { + // The login screen is immediately displayed on top of + // the Shrine home screen using onGenerateRoute and so + // rootNavigator must be set to true in order to get out + // of Shrine completely. + Navigator.of(context, rootNavigator: true).pop(); + }, + child: Padding( + padding: buttonTextPadding, + child: Text( + GalleryLocalizations.of(context)!.shrineCancelButtonCaption, + style: TextStyle(color: colorScheme.onSurface), + ), + ), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 8, + shape: const BeveledRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(7)), + ), + ), + onPressed: () { + Navigator.of(context).restorablePushNamed(ShrineApp.homeRoute); + }, + child: Padding( + padding: buttonTextPadding, + child: Text( + GalleryLocalizations.of(context)!.shrineNextButtonCaption, + style: TextStyle( + letterSpacing: letterSpacingOrNone(largeLetterSpacing)), + ), + ), + ), + ], + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/shrine/model/app_state_model.dart b/dev/integration_tests/new_gallery/lib/studies/shrine/model/app_state_model.dart new file mode 100644 index 0000000000..83185e75cb --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/shrine/model/app_state_model.dart @@ -0,0 +1,123 @@ +// 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:scoped_model/scoped_model.dart'; + +import 'product.dart'; +import 'products_repository.dart'; + +double _salesTaxRate = 0.06; +double _shippingCostPerItem = 7; + +class AppStateModel extends Model { + // All the available products. + List _availableProducts = []; + + // The currently selected category of products. + Category _selectedCategory = categoryAll; + + // The IDs and quantities of products currently in the cart. + final Map _productsInCart = {}; + + Map get productsInCart => Map.from(_productsInCart); + + // Total number of items in the cart. + int get totalCartQuantity => _productsInCart.values.fold(0, (int v, int e) => v + e); + + Category get selectedCategory => _selectedCategory; + + // Totaled prices of the items in the cart. + double get subtotalCost { + return _productsInCart.keys + .map((int id) => _availableProducts[id].price * _productsInCart[id]!) + .fold(0.0, (double sum, int e) => sum + e); + } + + // Total shipping cost for the items in the cart. + double get shippingCost { + return _shippingCostPerItem * + _productsInCart.values.fold(0.0, (num sum, int e) => sum + e); + } + + // Sales tax for the items in the cart + double get tax => subtotalCost * _salesTaxRate; + + // Total cost to order everything in the cart. + double get totalCost => subtotalCost + shippingCost + tax; + + // Returns a copy of the list of available products, filtered by category. + List getProducts() { + if (_selectedCategory == categoryAll) { + return List.from(_availableProducts); + } else { + return _availableProducts + .where((Product p) => p.category == _selectedCategory) + .toList(); + } + } + + // Adds a product to the cart. + void addProductToCart(int productId) { + if (!_productsInCart.containsKey(productId)) { + _productsInCart[productId] = 1; + } else { + _productsInCart[productId] = _productsInCart[productId]! + 1; + } + + notifyListeners(); + } + + // Adds products to the cart by a certain amount. + // quantity must be non-null positive value. + void addMultipleProductsToCart(int productId, int quantity) { + assert(quantity > 0); + if (!_productsInCart.containsKey(productId)) { + _productsInCart[productId] = quantity; + } else { + _productsInCart[productId] = _productsInCart[productId]! + quantity; + } + + notifyListeners(); + } + + // Removes an item from the cart. + void removeItemFromCart(int productId) { + if (_productsInCart.containsKey(productId)) { + if (_productsInCart[productId] == 1) { + _productsInCart.remove(productId); + } else { + _productsInCart[productId] = _productsInCart[productId]! - 1; + } + } + + notifyListeners(); + } + + // Returns the Product instance matching the provided id. + Product getProductById(int id) { + return _availableProducts.firstWhere((Product p) => p.id == id); + } + + // Removes everything from the cart. + void clearCart() { + _productsInCart.clear(); + notifyListeners(); + } + + // Loads the list of available products from the repo. + void loadProducts() { + _availableProducts = ProductsRepository.loadProducts(categoryAll); + notifyListeners(); + } + + void setCategory(Category newCategory) { + _selectedCategory = newCategory; + notifyListeners(); + } + + @override + String toString() { + return 'AppStateModel(totalCost: $totalCost)'; + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/shrine/model/product.dart b/dev/integration_tests/new_gallery/lib/studies/shrine/model/product.dart new file mode 100644 index 0000000000..6a899d98ea --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/shrine/model/product.dart @@ -0,0 +1,68 @@ +// 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:flutter/material.dart'; + +import '../../../gallery_localizations.dart'; + +class Category { + const Category({ + required this.name, + }); + + // A function taking a BuildContext as input and + // returns the internationalized name of the category. + final String Function(BuildContext) name; +} + +Category categoryAll = Category( + name: (BuildContext context) => GalleryLocalizations.of(context)!.shrineCategoryNameAll, +); + +Category categoryAccessories = Category( + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineCategoryNameAccessories, +); + +Category categoryClothing = Category( + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineCategoryNameClothing, +); + +Category categoryHome = Category( + name: (BuildContext context) => GalleryLocalizations.of(context)!.shrineCategoryNameHome, +); + +List categories = [ + categoryAll, + categoryAccessories, + categoryClothing, + categoryHome, +]; + +class Product { + const Product({ + required this.category, + required this.id, + required this.isFeatured, + required this.name, + required this.price, + this.assetAspectRatio = 1, + }); + + final Category category; + final int id; + final bool isFeatured; + final double assetAspectRatio; + + // A function taking a BuildContext as input and + // returns the internationalized name of the product. + final String Function(BuildContext) name; + + final int price; + + String get assetName => '$id-0.jpg'; + + String get assetPackage => 'shrine_images'; +} diff --git a/dev/integration_tests/new_gallery/lib/studies/shrine/model/products_repository.dart b/dev/integration_tests/new_gallery/lib/studies/shrine/model/products_repository.dart new file mode 100644 index 0000000000..61fe1d0a14 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/shrine/model/products_repository.dart @@ -0,0 +1,361 @@ +// 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:flutter/widgets.dart'; +import '../../../gallery_localizations.dart'; +import 'product.dart'; + +class ProductsRepository { + static List loadProducts(Category category) { + final List allProducts = [ + Product( + category: categoryAccessories, + id: 0, + isFeatured: true, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductVagabondSack, + price: 120, + assetAspectRatio: 329 / 246, + ), + Product( + category: categoryAccessories, + id: 1, + isFeatured: true, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductStellaSunglasses, + price: 58, + assetAspectRatio: 329 / 247, + ), + Product( + category: categoryAccessories, + id: 2, + isFeatured: false, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductWhitneyBelt, + price: 35, + assetAspectRatio: 329 / 228, + ), + Product( + category: categoryAccessories, + id: 3, + isFeatured: true, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductGardenStrand, + price: 98, + assetAspectRatio: 329 / 246, + ), + Product( + category: categoryAccessories, + id: 4, + isFeatured: false, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductStrutEarrings, + price: 34, + assetAspectRatio: 329 / 246, + ), + Product( + category: categoryAccessories, + id: 5, + isFeatured: false, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductVarsitySocks, + price: 12, + assetAspectRatio: 329 / 246, + ), + Product( + category: categoryAccessories, + id: 6, + isFeatured: false, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductWeaveKeyring, + price: 16, + assetAspectRatio: 329 / 246, + ), + Product( + category: categoryAccessories, + id: 7, + isFeatured: true, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductGatsbyHat, + price: 40, + assetAspectRatio: 329 / 246, + ), + Product( + category: categoryAccessories, + id: 8, + isFeatured: true, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductShrugBag, + price: 198, + assetAspectRatio: 329 / 246, + ), + Product( + category: categoryHome, + id: 9, + isFeatured: true, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductGiltDeskTrio, + price: 58, + assetAspectRatio: 329 / 246, + ), + Product( + category: categoryHome, + id: 10, + isFeatured: false, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductCopperWireRack, + price: 18, + assetAspectRatio: 329 / 246, + ), + Product( + category: categoryHome, + id: 11, + isFeatured: false, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductSootheCeramicSet, + price: 28, + assetAspectRatio: 329 / 247, + ), + Product( + category: categoryHome, + id: 12, + isFeatured: false, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductHurrahsTeaSet, + price: 34, + assetAspectRatio: 329 / 213, + ), + Product( + category: categoryHome, + id: 13, + isFeatured: true, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductBlueStoneMug, + price: 18, + assetAspectRatio: 329 / 246, + ), + Product( + category: categoryHome, + id: 14, + isFeatured: true, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductRainwaterTray, + price: 27, + assetAspectRatio: 329 / 246, + ), + Product( + category: categoryHome, + id: 15, + isFeatured: true, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductChambrayNapkins, + price: 16, + assetAspectRatio: 329 / 246, + ), + Product( + category: categoryHome, + id: 16, + isFeatured: true, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductSucculentPlanters, + price: 16, + assetAspectRatio: 329 / 246, + ), + Product( + category: categoryHome, + id: 17, + isFeatured: false, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductQuartetTable, + price: 175, + assetAspectRatio: 329 / 246, + ), + Product( + category: categoryHome, + id: 18, + isFeatured: true, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductKitchenQuattro, + price: 129, + assetAspectRatio: 329 / 246, + ), + Product( + category: categoryClothing, + id: 19, + isFeatured: false, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductClaySweater, + price: 48, + assetAspectRatio: 329 / 219, + ), + Product( + category: categoryClothing, + id: 20, + isFeatured: false, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductSeaTunic, + price: 45, + assetAspectRatio: 329 / 221, + ), + Product( + category: categoryClothing, + id: 21, + isFeatured: false, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductPlasterTunic, + price: 38, + assetAspectRatio: 220 / 329, + ), + Product( + category: categoryClothing, + id: 22, + isFeatured: false, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductWhitePinstripeShirt, + price: 70, + assetAspectRatio: 219 / 329, + ), + Product( + category: categoryClothing, + id: 23, + isFeatured: false, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductChambrayShirt, + price: 70, + assetAspectRatio: 329 / 221, + ), + Product( + category: categoryClothing, + id: 24, + isFeatured: true, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductSeabreezeSweater, + price: 60, + assetAspectRatio: 220 / 329, + ), + Product( + category: categoryClothing, + id: 25, + isFeatured: false, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductGentryJacket, + price: 178, + assetAspectRatio: 329 / 219, + ), + Product( + category: categoryClothing, + id: 26, + isFeatured: false, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductNavyTrousers, + price: 74, + assetAspectRatio: 220 / 329, + ), + Product( + category: categoryClothing, + id: 27, + isFeatured: true, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductWalterHenleyWhite, + price: 38, + assetAspectRatio: 219 / 329, + ), + Product( + category: categoryClothing, + id: 28, + isFeatured: true, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductSurfAndPerfShirt, + price: 48, + assetAspectRatio: 329 / 219, + ), + Product( + category: categoryClothing, + id: 29, + isFeatured: true, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductGingerScarf, + price: 98, + assetAspectRatio: 219 / 329, + ), + Product( + category: categoryClothing, + id: 30, + isFeatured: true, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductRamonaCrossover, + price: 68, + assetAspectRatio: 220 / 329, + ), + Product( + category: categoryClothing, + id: 31, + isFeatured: false, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductChambrayShirt, + price: 38, + assetAspectRatio: 329 / 223, + ), + Product( + category: categoryClothing, + id: 32, + isFeatured: false, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductClassicWhiteCollar, + price: 58, + assetAspectRatio: 221 / 329, + ), + Product( + category: categoryClothing, + id: 33, + isFeatured: true, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductCeriseScallopTee, + price: 42, + assetAspectRatio: 329 / 219, + ), + Product( + category: categoryClothing, + id: 34, + isFeatured: false, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductShoulderRollsTee, + price: 27, + assetAspectRatio: 220 / 329, + ), + Product( + category: categoryClothing, + id: 35, + isFeatured: false, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductGreySlouchTank, + price: 24, + assetAspectRatio: 222 / 329, + ), + Product( + category: categoryClothing, + id: 36, + isFeatured: false, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductSunshirtDress, + price: 58, + assetAspectRatio: 219 / 329, + ), + Product( + category: categoryClothing, + id: 37, + isFeatured: true, + name: (BuildContext context) => + GalleryLocalizations.of(context)!.shrineProductFineLinesTee, + price: 58, + assetAspectRatio: 219 / 329, + ), + ]; + if (category == categoryAll) { + return allProducts; + } else { + return allProducts.where((Product p) => p.category == category).toList(); + } + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/shrine/page_status.dart b/dev/integration_tests/new_gallery/lib/studies/shrine/page_status.dart new file mode 100644 index 0000000000..a04d4976a7 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/shrine/page_status.dart @@ -0,0 +1,50 @@ +// 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:flutter/material.dart'; + +import '../../layout/adaptive.dart'; + +class PageStatus extends InheritedWidget { + const PageStatus({ + super.key, + required this.cartController, + required this.menuController, + required super.child, + }); + + final AnimationController cartController; + final AnimationController menuController; + + static PageStatus? of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); + } + + @override + bool updateShouldNotify(PageStatus oldWidget) => + oldWidget.cartController != cartController || + oldWidget.menuController != menuController; +} + +bool productPageIsVisible(BuildContext context) { + return _cartControllerOf(context).isDismissed && + (_menuControllerOf(context).isCompleted || isDisplayDesktop(context)); +} + +bool menuPageIsVisible(BuildContext context) { + return _cartControllerOf(context).isDismissed && + (_menuControllerOf(context).isDismissed || isDisplayDesktop(context)); +} + +bool cartPageIsVisible(BuildContext context) { + return _cartControllerOf(context).isCompleted; +} + +AnimationController _cartControllerOf(BuildContext context) { + return PageStatus.of(context)!.cartController; +} + +AnimationController _menuControllerOf(BuildContext context) { + return PageStatus.of(context)!.menuController; +} diff --git a/dev/integration_tests/new_gallery/lib/studies/shrine/routes.dart b/dev/integration_tests/new_gallery/lib/studies/shrine/routes.dart new file mode 100644 index 0000000000..efd784f302 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/shrine/routes.dart @@ -0,0 +1,6 @@ +// 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. + +const String loginRoute = '/shrine/login'; +const String homeRoute = '/shrine'; diff --git a/dev/integration_tests/new_gallery/lib/studies/shrine/scrim.dart b/dev/integration_tests/new_gallery/lib/studies/shrine/scrim.dart new file mode 100644 index 0000000000..053d2d7d9d --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/shrine/scrim.dart @@ -0,0 +1,48 @@ +// 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:flutter/material.dart'; + +class Scrim extends StatelessWidget { + const Scrim({super.key, required this.controller}); + + final AnimationController controller; + + @override + Widget build(BuildContext context) { + final Size deviceSize = MediaQuery.of(context).size; + return ExcludeSemantics( + child: AnimatedBuilder( + animation: controller, + builder: (BuildContext context, Widget? child) { + final Color color = + const Color(0xFFFFF0EA).withOpacity(controller.value * 0.87); + + final Widget scrimRectangle = Container( + width: deviceSize.width, height: deviceSize.height, color: color); + + final bool ignorePointer = + (controller.status == AnimationStatus.dismissed); + final bool tapToRevert = (controller.status == AnimationStatus.completed); + + if (tapToRevert) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + controller.reverse(); + }, + child: scrimRectangle, + ), + ); + } else if (ignorePointer) { + return IgnorePointer(child: scrimRectangle); + } else { + return scrimRectangle; + } + }, + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/shrine/shopping_cart.dart b/dev/integration_tests/new_gallery/lib/studies/shrine/shopping_cart.dart new file mode 100644 index 0000000000..8636f3a0b0 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/shrine/shopping_cart.dart @@ -0,0 +1,352 @@ +// 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:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:intl/intl.dart'; +import 'package:scoped_model/scoped_model.dart'; + +import '../../gallery_localizations.dart'; +import '../../layout/letter_spacing.dart'; +import 'colors.dart'; +import 'expanding_bottom_sheet.dart'; +import 'model/app_state_model.dart'; +import 'model/product.dart'; +import 'theme.dart'; + +const double _startColumnWidth = 60.0; +const String _ordinalSortKeyName = 'shopping_cart'; + +class ShoppingCartPage extends StatefulWidget { + const ShoppingCartPage({super.key}); + + @override + State createState() => _ShoppingCartPageState(); +} + +class _ShoppingCartPageState extends State { + List _createShoppingCartRows(AppStateModel model) { + return model.productsInCart.keys + .map( + (int id) => ShoppingCartRow( + product: model.getProductById(id), + quantity: model.productsInCart[id], + onPressed: () { + model.removeItemFromCart(id); + }, + ), + ) + .toList(); + } + + @override + Widget build(BuildContext context) { + final ThemeData localTheme = Theme.of(context); + return Scaffold( + backgroundColor: shrinePink50, + body: SafeArea( + child: ScopedModelDescendant( + builder: (BuildContext context, Widget? child, AppStateModel model) { + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + final ExpandingBottomSheetState? 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 TextStyle smallAmountStyle = + Theme.of(context).textTheme.bodyMedium!.copyWith(color: shrineBrown600); + final TextStyle largeAmountStyle = Theme.of(context) + .textTheme + .headlineMedium! + .copyWith(letterSpacing: letterSpacingOrNone(mediumLetterSpacing)); + final NumberFormat formatter = NumberFormat.simpleCurrency( + decimalDigits: 2, + locale: Localizations.localeOf(context).toString(), + ); + final GalleryLocalizations 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( + 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 NumberFormat formatter = NumberFormat.simpleCurrency( + decimalDigits: 0, + locale: Localizations.localeOf(context).toString(), + ); + final ThemeData localTheme = Theme.of(context); + + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + key: ValueKey(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, + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/shrine/supplemental/asymmetric_view.dart b/dev/integration_tests/new_gallery/lib/studies/shrine/supplemental/asymmetric_view.dart new file mode 100644 index 0000000000..7a79c2f08e --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/shrine/supplemental/asymmetric_view.dart @@ -0,0 +1,297 @@ +// 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 'dart:math'; + +import 'package:flutter/material.dart'; + +import '../../../data/gallery_options.dart'; +import '../../../layout/text_scale.dart'; +import '../category_menu_page.dart'; +import '../model/product.dart'; +import '../page_status.dart'; +import 'balanced_layout.dart'; +import 'desktop_product_columns.dart'; +import 'product_card.dart'; +import 'product_columns.dart'; + +const double _topPadding = 34.0; +const double _bottomPadding = 44.0; + +const double _cardToScreenWidthRatio = 0.59; + +class MobileAsymmetricView extends StatelessWidget { + const MobileAsymmetricView({ + super.key, + required this.products, + }); + + final List products; + + List _buildColumns( + BuildContext context, + BoxConstraints constraints, + ) { + if (products.isEmpty) { + return const []; + } + + // Decide whether the page size and text size allow 2-column products. + + final double cardHeight = (constraints.biggest.height - + _topPadding - + _bottomPadding - + TwoProductCardColumn.spacerHeight) / + 2; + + final double imageWidth = _cardToScreenWidthRatio * constraints.biggest.width - + TwoProductCardColumn.horizontalPadding; + + final double imageHeight = cardHeight - + MobileProductCard.defaultTextBoxHeight * + GalleryOptions.of(context).textScaleFactor(context); + + final bool shouldUseAlternatingLayout = + imageHeight > 0 && imageWidth / imageHeight < 49 / 33; + + if (shouldUseAlternatingLayout) { + // Alternating layout: a layout of alternating 2-product + // and 1-product columns. + // + // This will return a list of columns. It will oscillate between the two + // kinds of columns. Even cases of the index (0, 2, 4, etc) will be + // TwoProductCardColumn and the odd cases will be OneProductCardColumn. + // + // Each pair of columns will advance us 3 products forward (2 + 1). That's + // some kinda awkward math so we use _evenCasesIndex and _oddCasesIndex as + // helpers for creating the index of the product list that will correspond + // to the index of the list of columns. + + return List.generate(_listItemCount(products.length), (int index) { + double width = _cardToScreenWidthRatio * MediaQuery.of(context).size.width; + Widget column; + if (index.isEven) { + /// Even cases + final int bottom = _evenCasesIndex(index); + column = TwoProductCardColumn( + bottom: products[bottom], + top: + products.length - 1 >= bottom + 1 ? products[bottom + 1] : null, + imageAspectRatio: imageWidth / imageHeight, + ); + width += 32; + } else { + /// Odd cases + column = OneProductCardColumn( + product: products[_oddCasesIndex(index)], + reverse: true, + ); + } + return SizedBox( + width: width, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: column, + ), + ); + }).toList(); + } else { + // Alternating layout: a layout of 1-product columns. + + return [ + for (final Product product in products) + SizedBox( + width: _cardToScreenWidthRatio * MediaQuery.of(context).size.width, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: OneProductCardColumn( + product: product, + reverse: false, + ), + ), + ) + ]; + } + } + + int _evenCasesIndex(int input) { + // The operator ~/ is a cool one. It's the truncating division operator. It + // divides the number and if there's a remainder / decimal, it cuts it off. + // This is like dividing and then casting the result to int. Also, it's + // functionally equivalent to floor() in this case. + return input ~/ 2 * 3; + } + + int _oddCasesIndex(int input) { + assert(input > 0); + return (input / 2).ceil() * 3 - 1; + } + + int _listItemCount(int totalItems) { + return (totalItems % 3 == 0) + ? totalItems ~/ 3 * 2 + : (totalItems / 3).ceil() * 2 - 1; + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: PageStatus.of(context)!.cartController, + builder: (BuildContext context, Widget? child) => AnimatedBuilder( + animation: PageStatus.of(context)!.menuController, + builder: (BuildContext context, Widget? child) => ExcludeSemantics( + excluding: !productPageIsVisible(context), + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return ListView( + restorationId: 'product_page_list_view', + scrollDirection: Axis.horizontal, + padding: const EdgeInsetsDirectional.fromSTEB( + 0, + _topPadding, + 16, + _bottomPadding, + ), + physics: const BouncingScrollPhysics(), + children: _buildColumns(context, constraints), + ); + }, + ), + ), + ), + ); + } +} + +class DesktopAsymmetricView extends StatelessWidget { + const DesktopAsymmetricView({ + super.key, + required this.products, + }); + + final List products; + + @override + Widget build(BuildContext context) { + // Determine the scale factor for the desktop asymmetric view. + + final double textScaleFactor = GalleryOptions.of(context).textScaleFactor(context); + + // When text is larger, the images becomes wider, but at half the rate. + final double imageScaleFactor = reducedTextScale(context); + + // When text is larger, horizontal padding becomes smaller. + final num paddingScaleFactor = textScaleFactor >= 1.5 ? 0.25 : 1; + + // Calculate number of columns + + final double sidebar = desktopCategoryMenuPageWidth(context: context); + final num minimumBoundaryWidth = 84 * paddingScaleFactor; + final double columnWidth = 186 * imageScaleFactor; + final double columnGapWidth = 24 * imageScaleFactor; + final double windowWidth = MediaQuery.of(context).size.width; + + final int idealColumnCount = max( + 1, + ((windowWidth + columnGapWidth - 2 * minimumBoundaryWidth - sidebar) / + (columnWidth + columnGapWidth)) + .floor(), + ); + + // Limit column width to fit within window when there is only one column. + final double actualColumnWidth = idealColumnCount == 1 + ? min( + columnWidth, + windowWidth - sidebar - 2 * minimumBoundaryWidth, + ) + : columnWidth; + + final int columnCount = min(idealColumnCount, max(products.length, 1)); + + return AnimatedBuilder( + animation: PageStatus.of(context)!.cartController, + builder: (BuildContext context, Widget? child) => ExcludeSemantics( + excluding: !productPageIsVisible(context), + child: DesktopColumns( + columnCount: columnCount, + products: products, + largeImageWidth: actualColumnWidth, + smallImageWidth: columnCount > 1 + ? columnWidth - columnGapWidth + : actualColumnWidth, + ), + ), + ); + } +} + +class DesktopColumns extends StatelessWidget { + const DesktopColumns({ + super.key, + required this.columnCount, + required this.products, + required this.largeImageWidth, + required this.smallImageWidth, + }); + + final int columnCount; + final List products; + final double largeImageWidth; + final double smallImageWidth; + + @override + Widget build(BuildContext context) { + final Widget gap = Container(width: 24); + + final List> productCardLists = balancedLayout( + context: context, + columnCount: columnCount, + products: products, + largeImageWidth: largeImageWidth, + smallImageWidth: smallImageWidth, + ); + + final List productCardColumns = List.generate( + columnCount, + (int column) { + final bool alignToEnd = (column.isOdd) || (column == columnCount - 1); + final bool startLarge = column.isOdd; + final bool lowerStart = column.isOdd; + return DesktopProductCardColumn( + alignToEnd: alignToEnd, + startLarge: startLarge, + lowerStart: lowerStart, + products: productCardLists[column], + largeImageWidth: largeImageWidth, + smallImageWidth: smallImageWidth, + ); + }, + ); + + return ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: [ + Container(height: 60), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacer(), + ...List.generate( + 2 * columnCount - 1, + (int generalizedColumnIndex) { + if (generalizedColumnIndex.isEven) { + return productCardColumns[generalizedColumnIndex ~/ 2]; + } else { + return gap; + } + }, + ), + const Spacer(), + ], + ), + Container(height: 60), + ], + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/shrine/supplemental/balanced_layout.dart b/dev/integration_tests/new_gallery/lib/studies/shrine/supplemental/balanced_layout.dart new file mode 100644 index 0000000000..fecbee208f --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/shrine/supplemental/balanced_layout.dart @@ -0,0 +1,239 @@ +// 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:flutter/material.dart'; + +import '../model/product.dart'; +import 'desktop_product_columns.dart'; +import 'layout_cache.dart'; + +/// A placeholder id for an empty element. See [_iterateUntilBalanced] +/// for more information. +const int _emptyElement = -1; + +/// To avoid infinite loops, improvements to the layout are only performed +/// when a column's height changes by more than +/// [_deviationImprovementThreshold] pixels. +const int _deviationImprovementThreshold = 10; + +/// Height of a product image, paired with the product's id. +class _TaggedHeightData { + const _TaggedHeightData({ + required this.index, + required this.height, + }); + + /// The id of the corresponding product. + final int index; + + /// The height of the product image. + final double height; +} + +/// Converts a set of [_TaggedHeightData] elements to a list, +/// and add an empty element. +/// Used for iteration. +List<_TaggedHeightData> _toListAndAddEmpty(Set<_TaggedHeightData> set) { + final List<_TaggedHeightData> result = List<_TaggedHeightData>.from(set); + result.add(const _TaggedHeightData(index: _emptyElement, height: 0)); + return result; +} + +/// Encode parameters for caching. +String _encodeParameters({ + required int columnCount, + required List products, + required double largeImageWidth, + required double smallImageWidth, +}) { + final String productString = + [for (final Product product in products) product.id.toString()].join(','); + return '$columnCount;$productString,$largeImageWidth,$smallImageWidth'; +} + +/// Given a layout, replace integers by their corresponding products. +List> _generateLayout({ + required List products, + required List> layout, +}) { + return >[ + for (final List column in layout) + [ + for (final int index in column) products[index], + ] + ]; +} + +/// Given [columnObjects], list of the set of objects in each column, +/// and [columnHeights], list of heights of each column, +/// [_iterateUntilBalanced] moves and swaps objects between columns +/// until their heights are sufficiently close to each other. +/// This prevents the layout having significant, avoidable gaps at the bottom. +void _iterateUntilBalanced( + List> columnObjects, + List columnHeights, +) { + int failedMoves = 0; + final int columnCount = columnObjects.length; + + // No need to rearrange a 1-column layout. + if (columnCount == 1) { + return; + } + + while (true) { + // Loop through all possible 2-combinations of columns. + for (int source = 0; source < columnCount; ++source) { + for (int target = source + 1; target < columnCount; ++target) { + // Tries to find an object A from source column + // and an object B from target column, such that switching them + // causes the height of the two columns to be closer. + + // A or B can be empty; in this case, moving an object from one + // column to the other is the best choice. + + bool success = false; + + final double bestHeight = (columnHeights[source] + columnHeights[target]) / 2; + final double scoreLimit = (columnHeights[source] - bestHeight).abs(); + + final List<_TaggedHeightData> sourceObjects = _toListAndAddEmpty(columnObjects[source]); + final List<_TaggedHeightData> targetObjects = _toListAndAddEmpty(columnObjects[target]); + + _TaggedHeightData? bestA, bestB; + double? bestScore; + + for (final _TaggedHeightData a in sourceObjects) { + for (final _TaggedHeightData b in targetObjects) { + if (a.index == _emptyElement && b.index == _emptyElement) { + continue; + } else { + final double score = + (columnHeights[source] - a.height + b.height - bestHeight) + .abs(); + if (score < scoreLimit - _deviationImprovementThreshold) { + success = true; + if (bestScore == null || score < bestScore) { + bestScore = score; + bestA = a; + bestB = b; + } + } + } + } + } + + if (!success) { + ++failedMoves; + } else { + failedMoves = 0; + + // Switch A and B. + if (bestA != null && bestA.index != _emptyElement) { + columnObjects[source].remove(bestA); + columnObjects[target].add(bestA); + } + if (bestB != null && bestB.index != _emptyElement) { + columnObjects[target].remove(bestB); + columnObjects[source].add(bestB); + } + columnHeights[source] += bestB!.height - bestA!.height; + columnHeights[target] += bestA.height - bestB.height; + } + + // If no two columns' heights can be made closer by switching + // elements, the layout is sufficiently balanced. + if (failedMoves >= columnCount * (columnCount - 1) ~/ 2) { + return; + } + } + } + } +} + +/// Given a list of numbers [data], representing the heights of each image, +/// and a list of numbers [biases], representing the heights of the space +/// above each column, [_balancedDistribution] returns a layout of [data] +/// so that the height of each column is sufficiently close to each other, +/// represented as a list of lists of integers, each integer being an ID +/// for a product. +List> _balancedDistribution({ + required int columnCount, + required List data, + required List biases, +}) { + assert(biases.length == columnCount); + + final List> columnObjects = List>.generate( + columnCount, (int column) => <_TaggedHeightData>{}); + + final List columnHeights = List.from(biases); + + for (int i = 0; i < data.length; ++i) { + final int column = i % columnCount; + columnHeights[column] += data[i]; + columnObjects[column].add(_TaggedHeightData(index: i, height: data[i])); + } + + _iterateUntilBalanced(columnObjects, columnHeights); + + return >[ + for (final Set<_TaggedHeightData> column in columnObjects) + [for (final _TaggedHeightData object in column) object.index]..sort(), + ]; +} + +/// Generates a balanced layout for [columnCount] columns, +/// with products specified by the list [products], +/// where the larger images have width [largeImageWidth] +/// and the smaller images have width [smallImageWidth]. +/// The current [context] is also given to allow caching. +List> balancedLayout({ + required BuildContext context, + required int columnCount, + required List products, + required double largeImageWidth, + required double smallImageWidth, +}) { + final String encodedParameters = _encodeParameters( + columnCount: columnCount, + products: products, + largeImageWidth: largeImageWidth, + smallImageWidth: smallImageWidth, + ); + + // Check if this layout is cached. + if (LayoutCache.of(context).containsKey(encodedParameters)) { + return _generateLayout( + products: products, + layout: LayoutCache.of(context)[encodedParameters]!, + ); + } + + final List productHeights = [ + for (final Product product in products) + 1 / product.assetAspectRatio * (largeImageWidth + smallImageWidth) / 2 + + productCardAdditionalHeight, + ]; + + final List> layout = _balancedDistribution( + columnCount: columnCount, + data: productHeights, + biases: List.generate( + columnCount, + (int column) => (column.isEven ? 0 : columnTopSpace), + ), + ); + + // Add tailored layout to cache. + + LayoutCache.of(context)[encodedParameters] = layout; + + final List> result = _generateLayout( + products: products, + layout: layout, + ); + + return result; +} diff --git a/dev/integration_tests/new_gallery/lib/studies/shrine/supplemental/cut_corners_border.dart b/dev/integration_tests/new_gallery/lib/studies/shrine/supplemental/cut_corners_border.dart new file mode 100644 index 0000000000..cdebec35de --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/shrine/supplemental/cut_corners_border.dart @@ -0,0 +1,124 @@ +// 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 'dart:ui' show lerpDouble; + +import 'package:flutter/material.dart'; + +class CutCornersBorder extends OutlineInputBorder { + const CutCornersBorder({ + super.borderSide, + super.borderRadius = const BorderRadius.all(Radius.circular(2)), + this.cut = 7, + super.gapPadding = 2, + }); + + @override + CutCornersBorder copyWith({ + BorderSide? borderSide, + BorderRadius? borderRadius, + double? gapPadding, + double? cut, + }) { + return CutCornersBorder( + borderSide: borderSide ?? this.borderSide, + borderRadius: borderRadius ?? this.borderRadius, + gapPadding: gapPadding ?? this.gapPadding, + cut: cut ?? this.cut, + ); + } + + final double cut; + + @override + ShapeBorder? lerpFrom(ShapeBorder? a, double t) { + if (a is CutCornersBorder) { + final CutCornersBorder outline = a; + return CutCornersBorder( + borderRadius: BorderRadius.lerp(outline.borderRadius, borderRadius, t)!, + borderSide: BorderSide.lerp(outline.borderSide, borderSide, t), + cut: cut, + gapPadding: outline.gapPadding, + ); + } + return super.lerpFrom(a, t); + } + + @override + ShapeBorder? lerpTo(ShapeBorder? b, double t) { + if (b is CutCornersBorder) { + final CutCornersBorder outline = b; + return CutCornersBorder( + borderRadius: BorderRadius.lerp(borderRadius, outline.borderRadius, t)!, + borderSide: BorderSide.lerp(borderSide, outline.borderSide, t), + cut: cut, + gapPadding: outline.gapPadding, + ); + } + return super.lerpTo(b, t); + } + + Path _notchedCornerPath(Rect center, [double start = 0, double extent = 0]) { + final Path path = Path(); + if (start > 0 || extent > 0) { + path.relativeMoveTo(extent + start, center.top); + _notchedSidesAndBottom(center, path); + path + ..lineTo(center.left + cut, center.top) + ..lineTo(start, center.top); + } else { + path.moveTo(center.left + cut, center.top); + _notchedSidesAndBottom(center, path); + path.lineTo(center.left + cut, center.top); + } + return path; + } + + Path _notchedSidesAndBottom(Rect center, Path path) { + return path + ..lineTo(center.right - cut, center.top) + ..lineTo(center.right, center.top + cut) + ..lineTo(center.right, center.top + center.height - cut) + ..lineTo(center.right - cut, center.top + center.height) + ..lineTo(center.left + cut, center.top + center.height) + ..lineTo(center.left, center.top + center.height - cut) + ..lineTo(center.left, center.top + cut); + } + + @override + void paint( + Canvas canvas, + Rect rect, { + double? gapStart, + double gapExtent = 0, + double gapPercentage = 0, + TextDirection? textDirection, + }) { + assert(gapPercentage >= 0 && gapPercentage <= 1); + + final Paint paint = borderSide.toPaint(); + final RRect outer = borderRadius.toRRect(rect); + if (gapStart == null || gapExtent <= 0 || gapPercentage == 0) { + canvas.drawPath(_notchedCornerPath(outer.middleRect), paint); + } else { + final double? extent = lerpDouble(0.0, gapExtent + gapPadding * 2, gapPercentage); + switch (textDirection!) { + case TextDirection.rtl: + { + final Path path = _notchedCornerPath( + outer.middleRect, gapStart + gapPadding - extent!, extent); + canvas.drawPath(path, paint); + break; + } + case TextDirection.ltr: + { + final Path path = _notchedCornerPath( + outer.middleRect, gapStart - gapPadding, extent!); + canvas.drawPath(path, paint); + break; + } + } + } + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/shrine/supplemental/desktop_product_columns.dart b/dev/integration_tests/new_gallery/lib/studies/shrine/supplemental/desktop_product_columns.dart new file mode 100644 index 0000000000..7a778c8ede --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/shrine/supplemental/desktop_product_columns.dart @@ -0,0 +1,83 @@ +// 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 'dart:math'; + +import 'package:flutter/material.dart'; + +import '../model/product.dart'; +import 'product_card.dart'; + +/// Height of the text below each product card. +const double productCardAdditionalHeight = 84.0 * 2; + +/// Height of the divider between product cards. +const double productCardDividerHeight = 84.0; + +/// Height of the space at the top of every other column. +const double columnTopSpace = 84.0; + +class DesktopProductCardColumn extends StatelessWidget { + const DesktopProductCardColumn({ + super.key, + required this.alignToEnd, + required this.startLarge, + required this.lowerStart, + required this.products, + required this.largeImageWidth, + required this.smallImageWidth, + }); + + final List products; + + final bool alignToEnd; + final bool startLarge; + final bool lowerStart; + + final double largeImageWidth; + final double smallImageWidth; + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { + final int currentColumnProductCount = products.length; + final int currentColumnWidgetCount = + max(2 * currentColumnProductCount - 1, 0); + + return SizedBox( + width: largeImageWidth, + child: Column( + crossAxisAlignment: + alignToEnd ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: [ + if (lowerStart) Container(height: columnTopSpace), + ...List.generate(currentColumnWidgetCount, (int index) { + Widget card; + if (index.isEven) { + // This is a product. + final int productCardIndex = index ~/ 2; + card = DesktopProductCard( + product: products[productCardIndex], + imageWidth: startLarge + ? ((productCardIndex.isEven) + ? largeImageWidth + : smallImageWidth) + : ((productCardIndex.isEven) + ? smallImageWidth + : largeImageWidth), + ); + } else { + // This is just a divider. + card = Container( + height: productCardDividerHeight, + ); + } + return RepaintBoundary(child: card); + }), + ], + ), + ); + }); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/shrine/supplemental/layout_cache.dart b/dev/integration_tests/new_gallery/lib/studies/shrine/supplemental/layout_cache.dart new file mode 100644 index 0000000000..2f0850ec35 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/shrine/supplemental/layout_cache.dart @@ -0,0 +1,22 @@ +// 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:flutter/material.dart'; + +class LayoutCache extends InheritedWidget { + const LayoutCache({ + super.key, + required this.layouts, + required super.child, + }); + + static Map>> of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType()!.layouts; + } + + final Map>> layouts; + + @override + bool updateShouldNotify(LayoutCache oldWidget) => true; +} diff --git a/dev/integration_tests/new_gallery/lib/studies/shrine/supplemental/product_card.dart b/dev/integration_tests/new_gallery/lib/studies/shrine/supplemental/product_card.dart new file mode 100644 index 0000000000..ff2c3a73a0 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/shrine/supplemental/product_card.dart @@ -0,0 +1,148 @@ +// 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:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:scoped_model/scoped_model.dart'; + +import '../../../gallery_localizations.dart'; +import '../../../layout/adaptive.dart'; +import '../../../layout/image_placeholder.dart'; +import '../model/app_state_model.dart'; +import '../model/product.dart'; + +class MobileProductCard extends StatelessWidget { + const MobileProductCard({ + super.key, + this.imageAspectRatio = 33 / 49, + required this.product, + }) : assert(imageAspectRatio > 0); + + final double imageAspectRatio; + final Product product; + + static const double defaultTextBoxHeight = 65; + + @override + Widget build(BuildContext context) { + return Semantics( + container: true, + button: true, + enabled: true, + child: _buildProductCard( + context: context, + product: product, + imageAspectRatio: imageAspectRatio, + ), + ); + } +} + +class DesktopProductCard extends StatelessWidget { + const DesktopProductCard({ + super.key, + required this.product, + required this.imageWidth, + }); + + final Product product; + final double imageWidth; + + @override + Widget build(BuildContext context) { + return _buildProductCard( + context: context, + product: product, + imageWidth: imageWidth, + ); + } +} + +Widget _buildProductCard({ + required BuildContext context, + required Product product, + double? imageWidth, + double? imageAspectRatio, +}) { + final bool isDesktop = isDisplayDesktop(context); + // In case of desktop , imageWidth is passed through [DesktopProductCard] in + // case of mobile imageAspectRatio is passed through [MobileProductCard]. + // Below assert is so that correct combination should always be present. + assert(isDesktop && imageWidth != null || + !isDesktop && imageAspectRatio != null); + + final NumberFormat formatter = NumberFormat.simpleCurrency( + decimalDigits: 0, + locale: Localizations.localeOf(context).toString(), + ); + final ThemeData theme = Theme.of(context); + final FadeInImagePlaceholder imageWidget = FadeInImagePlaceholder( + image: AssetImage(product.assetName, package: product.assetPackage), + placeholder: Container( + color: Colors.black.withOpacity(0.1), + width: imageWidth, + height: imageWidth == null ? null : imageWidth / product.assetAspectRatio, + ), + fit: BoxFit.cover, + width: isDesktop ? imageWidth : null, + height: isDesktop ? null : double.infinity, + excludeFromSemantics: true, + ); + + return ScopedModelDescendant( + builder: (BuildContext context, Widget? child, AppStateModel model) { + return Semantics( + hint: GalleryLocalizations.of(context)! + .shrineScreenReaderProductAddToCart, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + model.addProductToCart(product.id); + }, + child: child, + ), + ), + ); + }, + child: Stack( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (isDesktop) imageWidget else AspectRatio( + aspectRatio: imageAspectRatio!, + child: imageWidget, + ), + SizedBox( + child: Column( + children: [ + const SizedBox(height: 23), + SizedBox( + width: imageWidth, + child: Text( + product.name(context), + style: theme.textTheme.labelLarge, + softWrap: true, + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 4), + Text( + formatter.format(product.price), + style: theme.textTheme.bodySmall, + ), + ], + ), + ), + ], + ), + const Padding( + padding: EdgeInsets.all(16), + child: Icon(Icons.add_shopping_cart), + ), + ], + ), + ); +} diff --git a/dev/integration_tests/new_gallery/lib/studies/shrine/supplemental/product_columns.dart b/dev/integration_tests/new_gallery/lib/studies/shrine/supplemental/product_columns.dart new file mode 100644 index 0000000000..ee27e0694b --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/shrine/supplemental/product_columns.dart @@ -0,0 +1,83 @@ +// 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:flutter/material.dart'; + +import '../model/product.dart'; +import 'product_card.dart'; + +class TwoProductCardColumn extends StatelessWidget { + const TwoProductCardColumn({ + super.key, + required this.bottom, + this.top, + required this.imageAspectRatio, + }); + + static const double spacerHeight = 44; + static const double horizontalPadding = 28; + + final Product bottom; + final Product? top; + final double imageAspectRatio; + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { + return ListView( + physics: const ClampingScrollPhysics(), + children: [ + Padding( + padding: const EdgeInsetsDirectional.only(start: horizontalPadding), + child: top != null + ? MobileProductCard( + imageAspectRatio: imageAspectRatio, + product: top!, + ) + : const SizedBox( + height: spacerHeight, + ), + ), + const SizedBox(height: spacerHeight), + Padding( + padding: const EdgeInsetsDirectional.only(end: horizontalPadding), + child: MobileProductCard( + imageAspectRatio: imageAspectRatio, + product: bottom, + ), + ), + ], + ); + }); + } +} + +class OneProductCardColumn extends StatelessWidget { + const OneProductCardColumn({ + super.key, + required this.product, + required this.reverse, + }); + + final Product product; + + // Whether the product column should align to the bottom. + final bool reverse; + + @override + Widget build(BuildContext context) { + return ListView( + physics: const ClampingScrollPhysics(), + reverse: reverse, + children: [ + const SizedBox( + height: 40, + ), + MobileProductCard( + product: product, + ), + ], + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/shrine/theme.dart b/dev/integration_tests/new_gallery/lib/studies/shrine/theme.dart new file mode 100644 index 0000000000..fe74ebebec --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/shrine/theme.dart @@ -0,0 +1,108 @@ +// 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:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import '../../layout/letter_spacing.dart'; +import 'colors.dart'; +import 'supplemental/cut_corners_border.dart'; + +const double defaultLetterSpacing = 0.03; +const double mediumLetterSpacing = 0.04; +const double largeLetterSpacing = 1.0; + +final ThemeData shrineTheme = _buildShrineTheme(); + +IconThemeData _customIconTheme(IconThemeData original) { + return original.copyWith(color: shrineBrown900); +} + +ThemeData _buildShrineTheme() { + final ThemeData base = ThemeData.light(); + return base.copyWith( + appBarTheme: const AppBarTheme( + systemOverlayStyle: SystemUiOverlayStyle.dark, + elevation: 0, + ), + scaffoldBackgroundColor: shrineBackgroundWhite, + cardColor: shrineBackgroundWhite, + primaryIconTheme: _customIconTheme(base.iconTheme), + inputDecorationTheme: const InputDecorationTheme( + border: CutCornersBorder( + borderSide: BorderSide(color: shrineBrown900, width: 0.5), + ), + contentPadding: EdgeInsets.symmetric(vertical: 20, horizontal: 16), + ), + textTheme: _buildShrineTextTheme(base.textTheme), + textSelectionTheme: const TextSelectionThemeData( + selectionColor: shrinePink100, + ), + primaryTextTheme: _buildShrineTextTheme(base.primaryTextTheme), + iconTheme: _customIconTheme(base.iconTheme), + colorScheme: _shrineColorScheme.copyWith( + error: shrineErrorRed, + primary: shrinePink100, + ), + ); +} + +TextTheme _buildShrineTextTheme(TextTheme base) { + return GoogleFonts.rubikTextTheme(base + .copyWith( + headlineSmall: base.headlineSmall!.copyWith( + fontWeight: FontWeight.w500, + letterSpacing: letterSpacingOrNone(defaultLetterSpacing), + ), + titleLarge: base.titleLarge!.copyWith( + fontSize: 18, + letterSpacing: letterSpacingOrNone(defaultLetterSpacing), + ), + bodySmall: base.bodySmall!.copyWith( + fontWeight: FontWeight.w400, + fontSize: 14, + letterSpacing: letterSpacingOrNone(defaultLetterSpacing), + ), + bodyLarge: base.bodyLarge!.copyWith( + fontWeight: FontWeight.w500, + fontSize: 16, + letterSpacing: letterSpacingOrNone(defaultLetterSpacing), + ), + bodyMedium: base.bodyMedium!.copyWith( + letterSpacing: letterSpacingOrNone(defaultLetterSpacing), + ), + titleMedium: base.titleMedium!.copyWith( + letterSpacing: letterSpacingOrNone(defaultLetterSpacing), + ), + headlineMedium: base.headlineMedium!.copyWith( + letterSpacing: letterSpacingOrNone(defaultLetterSpacing), + ), + labelLarge: base.labelLarge!.copyWith( + fontWeight: FontWeight.w500, + fontSize: 14, + letterSpacing: letterSpacingOrNone(defaultLetterSpacing), + ), + ) + .apply( + displayColor: shrineBrown900, + bodyColor: shrineBrown900, + )); +} + +const ColorScheme _shrineColorScheme = ColorScheme( + primary: shrinePink100, + primaryContainer: shrineBrown900, + secondary: shrinePink50, + secondaryContainer: shrineBrown900, + surface: shrineSurfaceWhite, + background: shrineBackgroundWhite, + error: shrineErrorRed, + onPrimary: shrineBrown900, + onSecondary: shrineBrown900, + onSurface: shrineBrown900, + onBackground: shrineBrown900, + onError: shrineSurfaceWhite, + brightness: Brightness.light, +); diff --git a/dev/integration_tests/new_gallery/lib/studies/shrine/triangle_category_indicator.dart b/dev/integration_tests/new_gallery/lib/studies/shrine/triangle_category_indicator.dart new file mode 100644 index 0000000000..d14a7e63f1 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/shrine/triangle_category_indicator.dart @@ -0,0 +1,49 @@ +// 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:flutter/material.dart'; + +import 'colors.dart'; + +const List _vertices = [ + Offset(0, -14), + Offset(-17, 14), + Offset(17, 14), + Offset(0, -14), + Offset(0, -7.37), + Offset(10.855, 10.48), + Offset(-10.855, 10.48), + Offset(0, -7.37), +]; + +class TriangleCategoryIndicator extends CustomPainter { + const TriangleCategoryIndicator( + this.triangleWidth, + this.triangleHeight, + ); + + final double triangleWidth; + final double triangleHeight; + + @override + void paint(Canvas canvas, Size size) { + final Path myPath = Path() + ..addPolygon( + List.from(_vertices.map((Offset vertex) { + return Offset(size.width, size.height) / 2 + + Offset(vertex.dx * triangleWidth / 34, + vertex.dy * triangleHeight / 28); + })), + true, + ); + final Paint myPaint = Paint()..color = shrinePink400; + canvas.drawPath(myPath, myPaint); + } + + @override + bool shouldRepaint(TriangleCategoryIndicator oldDelegate) => false; + + @override + bool shouldRebuildSemantics(TriangleCategoryIndicator oldDelegate) => false; +} diff --git a/dev/integration_tests/new_gallery/lib/studies/starter/app.dart b/dev/integration_tests/new_gallery/lib/studies/starter/app.dart new file mode 100644 index 0000000000..5a587e4bc5 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/starter/app.dart @@ -0,0 +1,68 @@ +// 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:flutter/material.dart'; + +import '../../data/gallery_options.dart'; +import '../../gallery_localizations.dart'; +import 'home.dart'; +import 'routes.dart' as routes; + +const Color _primaryColor = Color(0xFF6200EE); + +class StarterApp extends StatelessWidget { + const StarterApp({super.key}); + + static const String defaultRoute = routes.defaultRoute; + + @override + Widget build(BuildContext context) { + return MaterialApp( + restorationScopeId: 'starter_app', + title: GalleryLocalizations.of(context)!.starterAppTitle, + debugShowCheckedModeBanner: false, + localizationsDelegates: GalleryLocalizations.localizationsDelegates, + supportedLocales: GalleryLocalizations.supportedLocales, + locale: GalleryOptions.of(context).locale, + initialRoute: StarterApp.defaultRoute, + routes: { + StarterApp.defaultRoute: (BuildContext context) => const _Home(), + }, + theme: ThemeData( + highlightColor: Colors.transparent, + colorScheme: const ColorScheme( + primary: _primaryColor, + primaryContainer: Color(0xFF3700B3), + secondary: Color(0xFF03DAC6), + secondaryContainer: Color(0xFF018786), + background: Colors.white, + surface: Colors.white, + onBackground: Colors.black, + error: Color(0xFFB00020), + onError: Colors.white, + onPrimary: Colors.white, + onSecondary: Colors.black, + onSurface: Colors.black, + brightness: Brightness.light, + ), + dividerTheme: const DividerThemeData( + thickness: 1, + color: Color(0xFFE5E5E5), + ), + platform: GalleryOptions.of(context).platform, + ), + ); + } +} + +class _Home extends StatelessWidget { + const _Home(); + + @override + Widget build(BuildContext context) { + return const ApplyTextOptions( + child: HomePage(), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/starter/home.dart b/dev/integration_tests/new_gallery/lib/studies/starter/home.dart new file mode 100644 index 0000000000..fb84874d9a --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/starter/home.dart @@ -0,0 +1,202 @@ +// 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:flutter/material.dart'; + +import '../../gallery_localizations.dart'; +import '../../layout/adaptive.dart'; + +const double appBarDesktopHeight = 128.0; + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + final ColorScheme colorScheme = Theme.of(context).colorScheme; + final bool isDesktop = isDisplayDesktop(context); + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + final SafeArea body = SafeArea( + child: Padding( + padding: isDesktop + ? const EdgeInsets.symmetric(horizontal: 72, vertical: 48) + : const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText( + localizations.starterAppGenericHeadline, + style: textTheme.displaySmall!.copyWith( + color: colorScheme.onSecondary, + ), + ), + const SizedBox(height: 10), + SelectableText( + localizations.starterAppGenericSubtitle, + style: textTheme.titleMedium, + ), + const SizedBox(height: 48), + SelectableText( + localizations.starterAppGenericBody, + style: textTheme.bodyLarge, + ), + ], + ), + ), + ); + + if (isDesktop) { + return Row( + children: [ + const ListDrawer(), + const VerticalDivider(width: 1), + Expanded( + child: Scaffold( + appBar: const AdaptiveAppBar( + isDesktop: true, + ), + body: body, + floatingActionButton: FloatingActionButton.extended( + heroTag: 'Extended Add', + onPressed: () {}, + label: Text( + localizations.starterAppGenericButton, + style: TextStyle(color: colorScheme.onSecondary), + ), + icon: Icon(Icons.add, color: colorScheme.onSecondary), + tooltip: localizations.starterAppTooltipAdd, + ), + ), + ), + ], + ); + } else { + return Scaffold( + appBar: const AdaptiveAppBar(), + body: body, + drawer: const ListDrawer(), + floatingActionButton: FloatingActionButton( + heroTag: 'Add', + onPressed: () {}, + tooltip: localizations.starterAppTooltipAdd, + child: Icon( + Icons.add, + color: Theme.of(context).colorScheme.onSecondary, + ), + ), + ); + } + } +} + +class AdaptiveAppBar extends StatelessWidget implements PreferredSizeWidget { + const AdaptiveAppBar({ + super.key, + this.isDesktop = false, + }); + + final bool isDesktop; + + @override + Size get preferredSize => isDesktop + ? const Size.fromHeight(appBarDesktopHeight) + : const Size.fromHeight(kToolbarHeight); + + @override + Widget build(BuildContext context) { + final ThemeData themeData = Theme.of(context); + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return AppBar( + automaticallyImplyLeading: !isDesktop, + title: isDesktop + ? null + : SelectableText(localizations.starterAppGenericTitle), + bottom: isDesktop + ? PreferredSize( + preferredSize: const Size.fromHeight(26), + child: Container( + alignment: AlignmentDirectional.centerStart, + margin: const EdgeInsetsDirectional.fromSTEB(72, 0, 0, 22), + child: SelectableText( + localizations.starterAppGenericTitle, + style: themeData.textTheme.titleLarge!.copyWith( + color: themeData.colorScheme.onPrimary, + ), + ), + ), + ) + : null, + actions: [ + IconButton( + icon: const Icon(Icons.share), + tooltip: localizations.starterAppTooltipShare, + onPressed: () {}, + ), + IconButton( + icon: const Icon(Icons.favorite), + tooltip: localizations.starterAppTooltipFavorite, + onPressed: () {}, + ), + IconButton( + icon: const Icon(Icons.search), + tooltip: localizations.starterAppTooltipSearch, + onPressed: () {}, + ), + ], + ); + } +} + +class ListDrawer extends StatefulWidget { + const ListDrawer({super.key}); + + @override + State createState() => _ListDrawerState(); +} + +class _ListDrawerState extends State { + static const int numItems = 9; + + int selectedItem = 0; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + final GalleryLocalizations localizations = GalleryLocalizations.of(context)!; + return Drawer( + child: SafeArea( + child: ListView( + children: [ + ListTile( + title: SelectableText( + localizations.starterAppTitle, + style: textTheme.titleLarge, + ), + subtitle: SelectableText( + localizations.starterAppGenericSubtitle, + style: textTheme.bodyMedium, + ), + ), + const Divider(), + ...Iterable.generate(numItems).toList().map((int i) { + return ListTile( + selected: i == selectedItem, + leading: const Icon(Icons.favorite), + title: Text( + localizations.starterAppDrawerItem(i + 1), + ), + onTap: () { + setState(() { + selectedItem = i; + }); + }, + ); + }), + ], + ), + ), + ); + } +} diff --git a/dev/integration_tests/new_gallery/lib/studies/starter/routes.dart b/dev/integration_tests/new_gallery/lib/studies/starter/routes.dart new file mode 100644 index 0000000000..3cf919bcbb --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/studies/starter/routes.dart @@ -0,0 +1,5 @@ +// 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. + +const String defaultRoute = '/starter'; diff --git a/dev/integration_tests/new_gallery/lib/themes/gallery_theme_data.dart b/dev/integration_tests/new_gallery/lib/themes/gallery_theme_data.dart new file mode 100644 index 0000000000..6c976d9618 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/themes/gallery_theme_data.dart @@ -0,0 +1,93 @@ +// 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:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class GalleryThemeData { + static const Color _lightFillColor = Colors.black; + static const Color _darkFillColor = Colors.white; + + static final Color _lightFocusColor = Colors.black.withOpacity(0.12); + static final Color _darkFocusColor = Colors.white.withOpacity(0.12); + + static ThemeData lightThemeData = + themeData(lightColorScheme, _lightFocusColor); + static ThemeData darkThemeData = themeData(darkColorScheme, _darkFocusColor); + + static ThemeData themeData(ColorScheme colorScheme, Color focusColor) { + return ThemeData( + colorScheme: colorScheme, + textTheme: _textTheme, + appBarTheme: AppBarTheme( + backgroundColor: colorScheme.background, + elevation: 0, + iconTheme: IconThemeData(color: colorScheme.primary), + ), + iconTheme: IconThemeData(color: colorScheme.onPrimary), + canvasColor: colorScheme.background, + scaffoldBackgroundColor: colorScheme.background, + highlightColor: Colors.transparent, + focusColor: focusColor, + snackBarTheme: SnackBarThemeData( + behavior: SnackBarBehavior.floating, + backgroundColor: Color.alphaBlend( + _lightFillColor.withOpacity(0.80), + _darkFillColor, + ), + contentTextStyle: _textTheme.titleMedium!.apply(color: _darkFillColor), + ), + ); + } + + static const ColorScheme lightColorScheme = ColorScheme( + primary: Color(0xFFB93C5D), + primaryContainer: Color(0xFF117378), + secondary: Color(0xFFEFF3F3), + secondaryContainer: Color(0xFFFAFBFB), + background: Color(0xFFE6EBEB), + surface: Color(0xFFFAFBFB), + onBackground: Colors.white, + error: _lightFillColor, + onError: _lightFillColor, + onPrimary: _lightFillColor, + onSecondary: Color(0xFF322942), + onSurface: Color(0xFF241E30), + brightness: Brightness.light, + ); + + static const ColorScheme darkColorScheme = ColorScheme( + primary: Color(0xFFFF8383), + primaryContainer: Color(0xFF1CDEC9), + secondary: Color(0xFF4D1F7C), + secondaryContainer: Color(0xFF451B6F), + background: Color(0xFF241E30), + surface: Color(0xFF1F1929), + onBackground: Color(0x0DFFFFFF), // White with 0.05 opacity + error: _darkFillColor, + onError: _darkFillColor, + onPrimary: _darkFillColor, + onSecondary: _darkFillColor, + onSurface: _darkFillColor, + brightness: Brightness.dark, + ); + + static const FontWeight _regular = FontWeight.w400; + static const FontWeight _medium = FontWeight.w500; + static const FontWeight _semiBold = FontWeight.w600; + static const FontWeight _bold = FontWeight.w700; + + static final TextTheme _textTheme = TextTheme( + headlineMedium: GoogleFonts.montserrat(fontWeight: _bold, fontSize: 20.0), + bodySmall: GoogleFonts.oswald(fontWeight: _semiBold, fontSize: 16.0), + headlineSmall: GoogleFonts.oswald(fontWeight: _medium, fontSize: 16.0), + titleMedium: GoogleFonts.montserrat(fontWeight: _medium, fontSize: 16.0), + labelSmall: GoogleFonts.montserrat(fontWeight: _medium, fontSize: 12.0), + bodyLarge: GoogleFonts.montserrat(fontWeight: _regular, fontSize: 14.0), + titleSmall: GoogleFonts.montserrat(fontWeight: _medium, fontSize: 14.0), + bodyMedium: GoogleFonts.montserrat(fontWeight: _regular, fontSize: 16.0), + titleLarge: GoogleFonts.montserrat(fontWeight: _bold, fontSize: 16.0), + labelLarge: GoogleFonts.montserrat(fontWeight: _semiBold, fontSize: 14.0), + ); +} diff --git a/dev/integration_tests/new_gallery/lib/themes/material_demo_theme_data.dart b/dev/integration_tests/new_gallery/lib/themes/material_demo_theme_data.dart new file mode 100644 index 0000000000..de2b98d276 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/themes/material_demo_theme_data.dart @@ -0,0 +1,87 @@ +// 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:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class MaterialDemoThemeData { + static final ThemeData themeData = ThemeData( + colorScheme: _colorScheme.copyWith( + background: Colors.white, + ), + canvasColor: _colorScheme.background, + highlightColor: Colors.transparent, + indicatorColor: _colorScheme.onPrimary, + scaffoldBackgroundColor: _colorScheme.background, + secondaryHeaderColor: _colorScheme.background, + typography: Typography.material2018( + platform: defaultTargetPlatform, + ), + visualDensity: VisualDensity.standard, + // Component themes + appBarTheme: AppBarTheme( + color: _colorScheme.primary, + iconTheme: IconThemeData(color: _colorScheme.onPrimary), + ), + bottomAppBarTheme: BottomAppBarTheme( + color: _colorScheme.primary, + ), + checkboxTheme: CheckboxThemeData( + fillColor: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return null; + } + return states.contains(MaterialState.selected) + ? _colorScheme.primary + : null; + }), + ), + radioTheme: RadioThemeData( + fillColor: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return null; + } + return states.contains(MaterialState.selected) + ? _colorScheme.primary + : null; + }), + ), + snackBarTheme: const SnackBarThemeData( + behavior: SnackBarBehavior.floating, + ), + switchTheme: SwitchThemeData( + thumbColor: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return null; + } + return states.contains(MaterialState.selected) + ? _colorScheme.primary + : null; + }), + trackColor: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return null; + } + return states.contains(MaterialState.selected) + ? _colorScheme.primary.withAlpha(0x80) + : null; + }), + )); + + static const ColorScheme _colorScheme = ColorScheme( + primary: Color(0xFF6200EE), + primaryContainer: Color(0xFF6200EE), + secondary: Color(0xFFFF5722), + secondaryContainer: Color(0xFFFF5722), + background: Colors.white, + surface: Color(0xFFF2F2F2), + onBackground: Colors.black, + onSurface: Colors.black, + error: Colors.red, + onError: Colors.white, + onPrimary: Colors.white, + onSecondary: Colors.white, + brightness: Brightness.light, + ); +} diff --git a/dev/integration_tests/new_gallery/pubspec.yaml b/dev/integration_tests/new_gallery/pubspec.yaml new file mode 100644 index 0000000000..bdddf96fdf --- /dev/null +++ b/dev/integration_tests/new_gallery/pubspec.yaml @@ -0,0 +1,316 @@ +name: gallery +description: A resource to help developers evaluate and use Flutter. +repository: https://github.com/flutter/flutter/dev/integration_tests/new_gallery +version: 2.10.2+021002 # See README.md for details on versioning. + +environment: + flutter: ^3.13.0 + sdk: ^3.1.0 + +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + adaptive_breakpoints: 0.1.7 + animations: 2.0.11 + collection: 1.18.0 + cupertino_icons: 1.0.6 + dual_screen: 1.0.4 + flutter_gallery_assets: 1.0.2 + flutter_localized_locales: 2.0.5 + flutter_staggered_grid_view: 0.7.0 + google_fonts: 4.0.4 + intl: 0.18.1 + meta: 1.12.0 + provider: 6.1.1 + rally_assets: 3.0.1 + scoped_model: 2.0.0 + shrine_images: 2.0.2 + url_launcher: 6.2.4 + vector_math: 2.1.4 + + async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + characters: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + crypto: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + ffi: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + http: 0.13.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + http_parser: 4.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + material_color_utilities: 0.8.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + nested: 1.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path: 1.9.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path_provider: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path_provider_android: 2.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path_provider_foundation: 2.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path_provider_linux: 2.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path_provider_platform_interface: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path_provider_windows: 2.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + platform: 3.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + plugin_platform_interface: 2.1.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + url_launcher_android: 6.2.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + url_launcher_ios: 6.2.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + url_launcher_linux: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + url_launcher_macos: 3.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + url_launcher_platform_interface: 2.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + url_launcher_web: 2.2.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + url_launcher_windows: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web: 0.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + win32: 5.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + xdg_directories: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_driver: + sdk: flutter + test: 1.25.2 + + _fe_analyzer_shared: 67.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + analyzer: 6.4.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + args: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + coverage: 1.7.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + fake_async: 1.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + file: 7.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + frontend_server_client: 3.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + glob: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + http_multi_server: 3.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + io: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + js: 0.6.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + leak_tracker: 10.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + leak_tracker_flutter_testing: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + leak_tracker_testing: 3.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + lints: 3.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + logging: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + matcher: 0.12.16+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + mime: 1.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + node_preamble: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + package_config: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + pool: 1.5.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + pub_semver: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + shelf: 1.4.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + shelf_packages_handler: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + shelf_static: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + shelf_web_socket: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.7.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_core: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 14.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + web_socket_channel: 2.4.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webdriver: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + +flutter: + uses-material-design: true + assets: + - packages/flutter_gallery_assets/crane/destinations/eat_0.jpg + - packages/flutter_gallery_assets/crane/destinations/eat_1.jpg + - packages/flutter_gallery_assets/crane/destinations/eat_2.jpg + - packages/flutter_gallery_assets/crane/destinations/eat_3.jpg + - packages/flutter_gallery_assets/crane/destinations/eat_4.jpg + - packages/flutter_gallery_assets/crane/destinations/eat_5.jpg + - packages/flutter_gallery_assets/crane/destinations/eat_6.jpg + - packages/flutter_gallery_assets/crane/destinations/eat_7.jpg + - packages/flutter_gallery_assets/crane/destinations/eat_8.jpg + - packages/flutter_gallery_assets/crane/destinations/eat_9.jpg + - packages/flutter_gallery_assets/crane/destinations/eat_10.jpg + - packages/flutter_gallery_assets/crane/destinations/fly_0.jpg + - packages/flutter_gallery_assets/crane/destinations/fly_1.jpg + - packages/flutter_gallery_assets/crane/destinations/fly_2.jpg + - packages/flutter_gallery_assets/crane/destinations/fly_3.jpg + - packages/flutter_gallery_assets/crane/destinations/fly_4.jpg + - packages/flutter_gallery_assets/crane/destinations/fly_5.jpg + - packages/flutter_gallery_assets/crane/destinations/fly_6.jpg + - packages/flutter_gallery_assets/crane/destinations/fly_7.jpg + - packages/flutter_gallery_assets/crane/destinations/fly_8.jpg + - packages/flutter_gallery_assets/crane/destinations/fly_9.jpg + - packages/flutter_gallery_assets/crane/destinations/fly_10.jpg + - packages/flutter_gallery_assets/crane/destinations/fly_11.jpg + - packages/flutter_gallery_assets/crane/destinations/fly_12.jpg + - packages/flutter_gallery_assets/crane/destinations/fly_13.jpg + - packages/flutter_gallery_assets/crane/destinations/sleep_0.jpg + - packages/flutter_gallery_assets/crane/destinations/sleep_1.jpg + - packages/flutter_gallery_assets/crane/destinations/sleep_2.jpg + - packages/flutter_gallery_assets/crane/destinations/sleep_3.jpg + - packages/flutter_gallery_assets/crane/destinations/sleep_4.jpg + - packages/flutter_gallery_assets/crane/destinations/sleep_5.jpg + - packages/flutter_gallery_assets/crane/destinations/sleep_6.jpg + - packages/flutter_gallery_assets/crane/destinations/sleep_7.jpg + - packages/flutter_gallery_assets/crane/destinations/sleep_8.jpg + - packages/flutter_gallery_assets/crane/destinations/sleep_9.jpg + - packages/flutter_gallery_assets/crane/destinations/sleep_10.jpg + - packages/flutter_gallery_assets/crane/destinations/sleep_11.jpg + - packages/flutter_gallery_assets/crane/logo/logo.png + - packages/flutter_gallery_assets/fonts/google_fonts/Raleway-Medium.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/Raleway-SemiBold.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/Raleway-Regular.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/Raleway-Light.ttf + - packages/flutter_gallery_assets/assets/studies/shrine_card_dark.png + - packages/flutter_gallery_assets/assets/studies/starter_card.png + - packages/flutter_gallery_assets/assets/studies/starter_card_dark.png + - packages/flutter_gallery_assets/assets/studies/fortnightly_card_dark.png + - packages/flutter_gallery_assets/assets/studies/rally_card_dark.png + - packages/flutter_gallery_assets/assets/studies/reply_card_dark.png + - packages/flutter_gallery_assets/assets/studies/fortnightly_card.png + - packages/flutter_gallery_assets/assets/studies/crane_card.png + - packages/flutter_gallery_assets/assets/studies/shrine_card.png + - packages/flutter_gallery_assets/assets/studies/crane_card_dark.png + - packages/flutter_gallery_assets/assets/studies/rally_card.png + - packages/flutter_gallery_assets/assets/studies/reply_card.png + - packages/flutter_gallery_assets/assets/logo/flutter_logo.png + - packages/flutter_gallery_assets/assets/logo/flutter_logo_color.png + - packages/flutter_gallery_assets/assets/icons/cupertino/cupertino.png + - packages/flutter_gallery_assets/assets/icons/material/material.png + - packages/flutter_gallery_assets/assets/icons/reference/reference.png + - packages/flutter_gallery_assets/assets/demos/bottom_navigation_background.png + - packages/flutter_gallery_assets/fonts/GalleryIcons.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/Merriweather-Regular.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/Eczar-Regular.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/Montserrat-Medium.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/Rubik-Bold.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/Merriweather-Light.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/RobotoCondensed-Bold.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/LibreFranklin-Regular.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/RobotoMono-Regular.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/LibreFranklin-ExtraBold.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/LibreFranklin-Bold.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/Oswald-SemiBold.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/Oswald-Medium.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/LibreFranklin-SemiBold.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/Montserrat-Bold.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/Merriweather-BoldItalic.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/Rubik-Medium.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/Montserrat-SemiBold.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/RobotoCondensed-Regular.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/LibreFranklin-Medium.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/Montserrat-Regular.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/Rubik-Regular.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/Eczar-SemiBold.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/WorkSans-Regular.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/WorkSans-Medium.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/WorkSans-Bold.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/WorkSans-Thin.ttf + - packages/flutter_gallery_assets/fonts/google_fonts/WorkSans-SemiBold.ttf + - packages/flutter_gallery_assets/fortnightly/fortnightly_army.png + - packages/flutter_gallery_assets/fortnightly/fortnightly_bees.jpg + - packages/flutter_gallery_assets/fortnightly/fortnightly_chart.png + - packages/flutter_gallery_assets/fortnightly/fortnightly_fabrics.png + - packages/flutter_gallery_assets/fortnightly/fortnightly_feminists.jpg + - packages/flutter_gallery_assets/fortnightly/fortnightly_gas.png + - packages/flutter_gallery_assets/fortnightly/fortnightly_healthcare.jpg + - packages/flutter_gallery_assets/fortnightly/fortnightly_stocks.png + - packages/flutter_gallery_assets/fortnightly/fortnightly_title.png + - packages/flutter_gallery_assets/fortnightly/fortnightly_war.png + - packages/flutter_gallery_assets/reply/attachments/paris_1.jpg + - packages/flutter_gallery_assets/reply/attachments/paris_2.jpg + - packages/flutter_gallery_assets/reply/attachments/paris_3.jpg + - packages/flutter_gallery_assets/reply/attachments/paris_4.jpg + - packages/flutter_gallery_assets/reply/avatars/avatar_0.jpg + - packages/flutter_gallery_assets/reply/avatars/avatar_1.jpg + - packages/flutter_gallery_assets/reply/avatars/avatar_2.jpg + - packages/flutter_gallery_assets/reply/avatars/avatar_3.jpg + - packages/flutter_gallery_assets/reply/avatars/avatar_4.jpg + - packages/flutter_gallery_assets/reply/avatars/avatar_5.jpg + - packages/flutter_gallery_assets/reply/avatars/avatar_6.jpg + - packages/flutter_gallery_assets/reply/avatars/avatar_7.jpg + - packages/flutter_gallery_assets/reply/avatars/avatar_8.jpg + - packages/flutter_gallery_assets/reply/avatars/avatar_9.jpg + - packages/flutter_gallery_assets/reply/avatars/avatar_10.jpg + - packages/flutter_gallery_assets/reply/avatars/avatar_express.png + - packages/flutter_gallery_assets/reply/icons/twotone_add_circle_outline.png + - packages/flutter_gallery_assets/reply/icons/twotone_delete.png + - packages/flutter_gallery_assets/reply/icons/twotone_drafts.png + - packages/flutter_gallery_assets/reply/icons/twotone_error.png + - packages/flutter_gallery_assets/reply/icons/twotone_folder.png + - packages/flutter_gallery_assets/reply/icons/twotone_forward.png + - packages/flutter_gallery_assets/reply/icons/twotone_inbox.png + - packages/flutter_gallery_assets/reply/icons/twotone_send.png + - packages/flutter_gallery_assets/reply/icons/twotone_star_on_background.png + - packages/flutter_gallery_assets/reply/icons/twotone_star.png + - packages/flutter_gallery_assets/reply/icons/twotone_stars.png + - packages/flutter_gallery_assets/reply/reply_logo.png + - packages/flutter_gallery_assets/places/india_chennai_flower_market.png + - packages/flutter_gallery_assets/places/india_thanjavur_market.png + - packages/flutter_gallery_assets/places/india_tanjore_bronze_works.png + - packages/flutter_gallery_assets/places/india_tanjore_market_merchant.png + - packages/flutter_gallery_assets/places/india_tanjore_thanjavur_temple.png + - packages/flutter_gallery_assets/places/india_pondicherry_salt_farm.png + - packages/flutter_gallery_assets/places/india_chennai_highway.png + - packages/flutter_gallery_assets/places/india_chettinad_silk_maker.png + - packages/flutter_gallery_assets/places/india_tanjore_thanjavur_temple_carvings.png + - packages/flutter_gallery_assets/places/india_chettinad_produce.png + - packages/flutter_gallery_assets/places/india_tanjore_market_technology.png + - packages/flutter_gallery_assets/places/india_pondicherry_beach.png + - packages/flutter_gallery_assets/places/india_pondicherry_fisherman.png + - packages/flutter_gallery_assets/placeholders/avatar_logo.png + - packages/flutter_gallery_assets/placeholders/placeholder_image.png + - packages/flutter_gallery_assets/splash_effects/splash_effect_1.gif + - packages/flutter_gallery_assets/splash_effects/splash_effect_2.gif + - packages/flutter_gallery_assets/splash_effects/splash_effect_3.gif + - packages/flutter_gallery_assets/splash_effects/splash_effect_4.gif + - packages/flutter_gallery_assets/splash_effects/splash_effect_5.gif + - packages/flutter_gallery_assets/splash_effects/splash_effect_6.gif + - packages/flutter_gallery_assets/splash_effects/splash_effect_7.gif + - packages/flutter_gallery_assets/splash_effects/splash_effect_8.gif + - packages/flutter_gallery_assets/splash_effects/splash_effect_9.gif + - packages/flutter_gallery_assets/splash_effects/splash_effect_10.gif + - packages/rally_assets/logo.png + - packages/rally_assets/thumb.png + - packages/shrine_images/diamond.png + - packages/shrine_images/slanted_menu.png + - packages/shrine_images/0-0.jpg + - packages/shrine_images/1-0.jpg + - packages/shrine_images/2-0.jpg + - packages/shrine_images/3-0.jpg + - packages/shrine_images/4-0.jpg + - packages/shrine_images/5-0.jpg + - packages/shrine_images/6-0.jpg + - packages/shrine_images/7-0.jpg + - packages/shrine_images/8-0.jpg + - packages/shrine_images/9-0.jpg + - packages/shrine_images/10-0.jpg + - packages/shrine_images/11-0.jpg + - packages/shrine_images/12-0.jpg + - packages/shrine_images/13-0.jpg + - packages/shrine_images/14-0.jpg + - packages/shrine_images/15-0.jpg + - packages/shrine_images/16-0.jpg + - packages/shrine_images/17-0.jpg + - packages/shrine_images/18-0.jpg + - packages/shrine_images/19-0.jpg + - packages/shrine_images/20-0.jpg + - packages/shrine_images/21-0.jpg + - packages/shrine_images/22-0.jpg + - packages/shrine_images/23-0.jpg + - packages/shrine_images/24-0.jpg + - packages/shrine_images/25-0.jpg + - packages/shrine_images/26-0.jpg + - packages/shrine_images/27-0.jpg + - packages/shrine_images/28-0.jpg + - packages/shrine_images/29-0.jpg + - packages/shrine_images/30-0.jpg + - packages/shrine_images/31-0.jpg + - packages/shrine_images/32-0.jpg + - packages/shrine_images/33-0.jpg + - packages/shrine_images/34-0.jpg + - packages/shrine_images/35-0.jpg + - packages/shrine_images/36-0.jpg + - packages/shrine_images/37-0.jpg + fonts: + - family: GalleryIcons + fonts: + - asset: packages/flutter_gallery_assets/fonts/GalleryIcons.ttf + +# PUBSPEC CHECKSUM: 27a7 diff --git a/dev/integration_tests/new_gallery/test/demo_descriptions_test.dart b/dev/integration_tests/new_gallery/test/demo_descriptions_test.dart new file mode 100644 index 0000000000..fe9cf2d6bd --- /dev/null +++ b/dev/integration_tests/new_gallery/test/demo_descriptions_test.dart @@ -0,0 +1,71 @@ +// 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:collection/collection.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:gallery/data/demos.dart'; +import 'package:gallery/gallery_localizations_en.dart'; + +bool _isUnique(List list) { + final Set covered = {}; + for (final String element in list) { + if (covered.contains(element)) { + return false; + } else { + covered.add(element); + } + } + return true; +} + +const ListEquality _stringListEquality = ListEquality(); + +void main() { + test('_isUnique works correctly', () { + expect(_isUnique(['a', 'b', 'c']), true); + expect(_isUnique(['a', 'c', 'a', 'b']), false); + expect(_isUnique(['a']), true); + expect(_isUnique([]), true); + }); + + test('Demo descriptions are unique and correct', () { + final List allDemos = Demos.all(GalleryLocalizationsEn()); + final List allDemoDescriptions = allDemos.map((GalleryDemo d) => d.describe).toList(); + + expect(_isUnique(allDemoDescriptions), true); + expect( + _stringListEquality.equals( + allDemoDescriptions, + Demos.allDescriptions(), + ), + true, + ); + }); + + test('Special demo descriptions are correct', () { + final List allDemos = Demos.allDescriptions(); + + final List specialDemos = [ + 'shrine@study', + 'rally@study', + 'crane@study', + 'fortnightly@study', + 'bottom-navigation@material', + 'button@material', + 'card@material', + 'chip@material', + 'dialog@material', + 'pickers@material', + 'cupertino-alerts@cupertino', + 'colors@other', + 'progress-indicator@material', + 'cupertino-activity-indicator@cupertino', + 'colors@other', + ]; + + for (final String specialDemo in specialDemos) { + expect(allDemos.contains(specialDemo), true); + } + }); +} diff --git a/dev/integration_tests/new_gallery/test/pages/home_test.dart b/dev/integration_tests/new_gallery/test/pages/home_test.dart new file mode 100644 index 0000000000..da0ca1b5ea --- /dev/null +++ b/dev/integration_tests/new_gallery/test/pages/home_test.dart @@ -0,0 +1,36 @@ +// 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:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:gallery/main.dart'; + +void main() { + testWidgets('Home page hides settings semantics when closed', (WidgetTester tester) async { + await tester.pumpWidget(const GalleryApp()); + + await tester.pump(const Duration(seconds: 1)); + + expect(find.bySemanticsLabel('Settings'), findsOneWidget); + expect(find.bySemanticsLabel('Close settings'), findsNothing); + + await tester.tap(find.bySemanticsLabel('Settings')); + await tester.pump(const Duration(seconds: 1)); + + // The test no longer finds Setting and Close settings since the semantics + // are excluded when settings mode is activated. + expect(find.bySemanticsLabel('Settings'), findsNothing); + expect(find.bySemanticsLabel('Close settings'), findsOneWidget); + }); + + testWidgets('Home page list view is the primary list view', (WidgetTester tester) async { + await tester.pumpWidget(const GalleryApp()); + await tester.pumpAndSettle(); + + final ListView listview = + tester.widget(find.byKey(const ValueKey('HomeListView'))); + + expect(listview.primary, true); + }); +} diff --git a/dev/integration_tests/new_gallery/test/theme_test.dart b/dev/integration_tests/new_gallery/test/theme_test.dart new file mode 100644 index 0000000000..390e3c2a46 --- /dev/null +++ b/dev/integration_tests/new_gallery/test/theme_test.dart @@ -0,0 +1,31 @@ +// 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:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:gallery/themes/material_demo_theme_data.dart'; + +void main() { + test('verify former toggleableActiveColor themes are set', () async { + const Color primaryColor = Color(0xFF6200EE); + final ThemeData themeData = MaterialDemoThemeData.themeData; + + expect( + themeData.checkboxTheme.fillColor!.resolve({MaterialState.selected}), + primaryColor, + ); + expect( + themeData.radioTheme.fillColor!.resolve({MaterialState.selected}), + primaryColor, + ); + expect( + themeData.switchTheme.thumbColor!.resolve({MaterialState.selected}), + primaryColor, + ); + expect( + themeData.switchTheme.trackColor!.resolve({MaterialState.selected}), + primaryColor.withOpacity(0.5), + ); + }); +} diff --git a/dev/integration_tests/new_gallery/test/utils.dart b/dev/integration_tests/new_gallery/test/utils.dart new file mode 100644 index 0000000000..9218e2ee8d --- /dev/null +++ b/dev/integration_tests/new_gallery/test/utils.dart @@ -0,0 +1,6 @@ +// 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. + +// Replace Windows line endings with Unix line endings +String standardizeLineEndings(String str) => str.replaceAll('\r\n', '\n'); diff --git a/dev/integration_tests/new_gallery/test/widget_test.dart b/dev/integration_tests/new_gallery/test/widget_test.dart new file mode 100644 index 0000000000..16d696cf8e --- /dev/null +++ b/dev/integration_tests/new_gallery/test/widget_test.dart @@ -0,0 +1,14 @@ +// 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:flutter_test/flutter_test.dart'; +import 'package:gallery/main.dart'; + +void main() { + testWidgets('Smoke test', (WidgetTester tester) async { + await tester.pumpWidget(const GalleryApp()); + await tester.pumpAndSettle(); + expect(find.text('Gallery'), findsOneWidget); + }); +} diff --git a/dev/integration_tests/new_gallery/test_driver/transitions_perf.dart b/dev/integration_tests/new_gallery/test_driver/transitions_perf.dart new file mode 100644 index 0000000000..6e577b44b9 --- /dev/null +++ b/dev/integration_tests/new_gallery/test_driver/transitions_perf.dart @@ -0,0 +1,32 @@ +// 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 'dart:async'; +import 'dart:convert' show JsonEncoder; + +import 'package:flutter/material.dart'; +import 'package:flutter_driver/driver_extension.dart'; +import 'package:gallery/data/demos.dart'; +import 'package:gallery/main.dart' show GalleryApp; + +// See transitions_perf_test.dart for how to run this test. + +Future _handleMessages(String? message) async { + switch (message) { + case 'demoDescriptions': + final List demoDescriptions = Demos.allDescriptions(); + return const JsonEncoder.withIndent(' ').convert(demoDescriptions); + case 'isTestingCraneOnly': + return const String.fromEnvironment('onlyCrane', defaultValue: 'false'); + case 'isTestingReplyOnly': + return const String.fromEnvironment('onlyReply', defaultValue: 'false'); + default: + throw 'unknown message'; + } +} + +void main() { + enableFlutterDriverExtension(handler: _handleMessages); + runApp(const GalleryApp(isTestMode: true)); +} diff --git a/dev/integration_tests/new_gallery/test_driver/transitions_perf_test.dart b/dev/integration_tests/new_gallery/test_driver/transitions_perf_test.dart new file mode 100644 index 0000000000..160fdea16d --- /dev/null +++ b/dev/integration_tests/new_gallery/test_driver/transitions_perf_test.dart @@ -0,0 +1,368 @@ +// 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 'dart:convert' show json; +import 'dart:io' show sleep, stdout; + +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:test/test.dart' hide TypeMatcher, isInstanceOf; + +// To run this test for all demos: +// flutter drive --profile --trace-startup -t test_driver/transitions_perf.dart -d +// To run this test for just Crane, with scrolling: +// flutter drive --profile --trace-startup -t test_driver/transitions_perf.dart -d --dart-define=onlyCrane=true +// To run this test for just Reply, with animations: +// flutter drive --profile --trace-startup -t test_driver/transitions_perf.dart -d --dart-define=onlyReply=true +// Enable semantics with the --with_semantics flag +// Note: The number of tests executed with timeline collection enabled +// significantly impacts heap size of the running app. When run with +// --trace-startup, as we do in this test, the VM stores trace events in an +// endless buffer instead of a ring buffer. + +// Demos for which timeline data will be collected using +// FlutterDriver.traceAction(). +// +// These names must match the output of GalleryDemo.describe in +// lib/data/demos.dart. +const List _profiledDemos = [ + 'reply@study', + 'shrine@study', + 'rally@study', + 'crane@study', + 'fortnightly@study', + 'bottom-navigation@material', + 'button@material', + 'card@material', + 'chip@material', + 'dialog@material', + 'pickers@material', + 'cupertino-alerts@cupertino', + 'colors@other', +]; + +// Demos that will be backed out of within FlutterDriver.runUnsynchronized(); +// +// These names must match the output of GalleryDemo.describe in +// lib/data/demos.dart. +const List _unsynchronizedDemos = [ + 'progress-indicator@material', + 'cupertino-activity-indicator@cupertino', + 'colors@other', +]; + +// Demos that will be not be launched. +// +// These names must match the output of GalleryDemo.describe in +// lib/data/demos.dart. +const List _skippedDemos = []; + +// All of the gallery demos, identified as "title@category". +// +// These names are reported by the test app, see _handleMessages() +// in transitions_perf.dart. +List _allDemos = []; + +// SerializableFinders for scrolling actions. +final SerializableFinder homeList = find.byValueKey('HomeListView'); +final SerializableFinder backButton = find.byValueKey('Back'); +final SerializableFinder galleryHeader = find.text('Gallery'); +final SerializableFinder categoriesHeader = find.text('Categories'); +final SerializableFinder craneFlyList = find.byValueKey('CraneListView-0'); + +// SerializableFinders for reply study actions. +final SerializableFinder replyFab = find.byValueKey('ReplyFab'); +final SerializableFinder replySearch = find.byValueKey('ReplySearch'); +final SerializableFinder replyEmail = find.byValueKey('ReplyEmail-0'); +final SerializableFinder replyLogo = find.byValueKey('ReplyLogo'); +final SerializableFinder replySentMailbox = find.byValueKey('Reply-Sent'); +final SerializableFinder replyExit = find.byValueKey('ReplyExit'); + +// Let overscroll animation settle on iOS after driver.scroll. +void handleOverscrollAnimation() { + sleep(const Duration(seconds: 1)); +} + +/// Scroll to the top of the app, given the current demo. Works with both mobile +/// and desktop layouts. +Future scrollToTop(SerializableFinder demoItem, FlutterDriver driver) async { + stdout.writeln('scrolling to top'); + + // Scroll to the Categories header. + await driver.scroll( + demoItem, + 0, + 5000, + const Duration(milliseconds: 200), + ); + handleOverscrollAnimation(); + + // Scroll to top. + await driver.scroll( + categoriesHeader, + 0, + 500, + const Duration(milliseconds: 200), + ); + handleOverscrollAnimation(); +} + +/// Returns a [Future] that resolves to true if the widget specified by [finder] +/// is present, false otherwise. +Future isPresent(SerializableFinder finder, FlutterDriver driver, + {Duration timeout = const Duration(seconds: 5)}) async { + try { + await driver.waitFor(finder, timeout: timeout); + return true; + } catch (exception) { + return false; + } +} + +/// Scrolls each demo into view, launches it, then returns to the +/// home screen, twice. +/// +/// Optionally specify a callback to perform further actions for each demo. +/// Optionally specify whether a scroll to top should be performed after the +/// demo has been opened twice (true by default). +Future runDemos( + List demos, + FlutterDriver driver, { + Future Function()? additionalActions, + bool scrollToTopWhenDone = true, +}) async { + String? currentDemoCategory; + late SerializableFinder demoList; + SerializableFinder? demoItem; + + for (final String demo in demos) { + if (_skippedDemos.contains(demo)) { + continue; + } + + stdout.writeln('> $demo'); + + final String demoCategory = demo.substring(demo.indexOf('@') + 1); + if (demoCategory != currentDemoCategory) { + // We've switched categories. + currentDemoCategory = demoCategory; + demoList = find.byValueKey('${demoCategory}DemoList'); + + // We may want to return to the previous category later. + // Reset its scroll (matters for desktop layout). + if (demoItem != null) { + await scrollToTop(demoItem, driver); + } + + // Scroll to the category list. + if (demoCategory != 'study') { + stdout.writeln('scrolling to $currentDemoCategory category'); + await driver.scrollUntilVisible( + homeList, + demoList, + dyScroll: -1000, + timeout: const Duration(seconds: 10), + ); + } + } + + // Scroll to demo and open it twice. + demoItem = find.byValueKey(demo); + + stdout.writeln('scrolling to demo'); + + // demoList below may be either the horizontally-scrolling Studies carousel + // or vertically scrolling Material/Cupertino/Other demo lists. + // + // The Studies carousel has scroll physics that snap items to the starting + // edge of the widget. TestDriver.scrollUntilVisible scrolls in increments + // along the x and y axes; if the distance is too small, the list snaps + // back to its previous position, if it's too large, it may scroll too far. + // To resolve this, we scroll 75% of the list width/height dimensions on + // each increment. + final DriverOffset topLeft = + await driver.getTopLeft(demoList, timeout: const Duration(seconds: 10)); + final DriverOffset bottomRight = await driver.getBottomRight(demoList, + timeout: const Duration(seconds: 10)); + final double listWidth = bottomRight.dx - topLeft.dx; + final double listHeight = bottomRight.dy - topLeft.dy; + await driver.scrollUntilVisible( + demoList, + demoItem, + dxScroll: -listWidth * 0.75, + dyScroll: -listHeight * 0.75, + alignment: 0.5, + timeout: const Duration(seconds: 10), + ); + + // We launch each demo twice to be able to measure and compare first and + // subsequent builds. + for (int i = 0; i < 2; i += 1) { + stdout.writeln('tapping demo'); + await driver.tap(demoItem); // Launch the demo + + sleep(const Duration(milliseconds: 500)); + + if (additionalActions != null) { + await additionalActions(); + } + + if (_unsynchronizedDemos.contains(demo)) { + await driver.runUnsynchronized(() async { + await driver.tap(backButton); + }); + } else { + await driver.tap(backButton); + } + } + stdout.writeln('< Success'); + } + + if (scrollToTopWhenDone) { + await scrollToTop(demoItem!, driver); + } +} + +void main([List args = const []]) { + group('Flutter Gallery transitions', () { + late FlutterDriver driver; + + late bool isTestingCraneOnly; + late bool isTestingReplyOnly; + + setUpAll(() async { + driver = await FlutterDriver.connect(); + + // See _handleMessages() in transitions_perf.dart. + _allDemos = List.from(json.decode( + await driver.requestData('demoDescriptions'), + ) as List); + if (_allDemos.isEmpty) { + throw 'no demo names found'; + } + + // See _handleMessages() in transitions_perf.dart. + isTestingCraneOnly = + await driver.requestData('isTestingCraneOnly') == 'true'; + + // See _handleMessages() in transitions_perf.dart. + isTestingReplyOnly = + await driver.requestData('isTestingReplyOnly') == 'true'; + + if (args.contains('--with_semantics')) { + stdout.writeln('Enabeling semantics...'); + await driver.setSemantics(true); + } + + await isPresent(galleryHeader, driver); + }); + + tearDownAll(() async { + await driver.close(); + + stdout.writeln( + 'Timeline summaries for profiled demos have been output to the build/ directory.'); + }); + + test('only Crane', () async { + if (!isTestingCraneOnly) { + return; + } + + // Collect timeline data for just the Crane study. + final Timeline timeline = await driver.traceAction( + () async { + await runDemos( + ['crane@study'], + driver, + additionalActions: () async => driver.scroll( + craneFlyList, + 0, + -1000, + const Duration(seconds: 1), + ), + scrollToTopWhenDone: false, + ); + }, + streams: const [ + TimelineStream.dart, + TimelineStream.embedder, + ], + ); + + final TimelineSummary summary = TimelineSummary.summarize(timeline); + await summary.writeTimelineToFile('transitions-crane', pretty: true); + }, timeout: Timeout.none); + + test('only Reply', () async { + if (!isTestingReplyOnly) { + return; + } + + // Collect timeline data for just the Crane study. + final Timeline timeline = await driver.traceAction( + () async { + await runDemos( + ['reply@study'], + driver, + additionalActions: () async { + // Tap compose fab to trigger open container transform/fade through + await driver.tap(replyFab); + // Exit compose page + await driver.tap(replyExit); + // Tap search icon to trigger shared axis transition + await driver.tap(replySearch); + // Exit search page + await driver.tap(replyExit); + // Tap on email to trigger open container transform + await driver.tap(replyEmail); + // Exit email page + await driver.tap(replyExit); + // Tap Reply logo to open bottom drawer/navigation rail + await driver.tap(replyLogo); + // Tap Reply logo to close bottom drawer/navigation rail + await driver.tap(replyLogo); + // Tap Reply logo to open bottom drawer/navigation rail + await driver.tap(replyLogo); + // Tap sent mailbox destination to trigger fade through transition + await driver.tap(replySentMailbox); + }, + scrollToTopWhenDone: false, + ); + }, + streams: const [ + TimelineStream.dart, + TimelineStream.embedder, + ], + ); + + final TimelineSummary summary = TimelineSummary.summarize(timeline); + await summary.writeTimelineToFile('transitions-reply', pretty: true); + }, timeout: Timeout.none); + + test('all demos', () async { + if (isTestingCraneOnly || isTestingReplyOnly) { + return; + } + + // Collect timeline data for just a limited set of demos to avoid OOMs. + final Timeline timeline = await driver.traceAction( + () async { + await runDemos(_profiledDemos, driver); + }, + streams: const [ + TimelineStream.dart, + TimelineStream.embedder, + ], + ); + + final TimelineSummary summary = TimelineSummary.summarize(timeline); + await summary.writeTimelineToFile('transitions', pretty: true); + + // Execute the remaining tests. + final Set unprofiledDemos = Set.from(_allDemos) + ..removeAll(_profiledDemos); + await runDemos(unprofiledDemos.toList(), driver); + }, timeout: Timeout.none); + }); +}