about.dart 55.7 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Ian Hickson's avatar
Ian Hickson committed
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:developer' show Timeline, Flow;
6
import 'dart:io' show Platform;
Ian Hickson's avatar
Ian Hickson committed
7

8
import 'package:flutter/foundation.dart';
9 10
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart' hide Flow;
Ian Hickson's avatar
Ian Hickson committed
11 12

import 'app_bar.dart';
13 14 15
import 'back_button.dart';
import 'card.dart';
import 'constants.dart';
Ian Hickson's avatar
Ian Hickson committed
16 17
import 'debug.dart';
import 'dialog.dart';
18
import 'divider.dart';
19 20 21
import 'floating_action_button.dart';
import 'floating_action_button_location.dart';
import 'ink_decoration.dart';
22
import 'list_tile.dart';
23
import 'material.dart';
24
import 'material_localizations.dart';
Ian Hickson's avatar
Ian Hickson committed
25
import 'page.dart';
26
import 'page_transitions_theme.dart';
Ian Hickson's avatar
Ian Hickson committed
27
import 'progress_indicator.dart';
Ian Hickson's avatar
Ian Hickson committed
28
import 'scaffold.dart';
29
import 'scrollbar.dart';
30
import 'text_button.dart';
31
import 'text_theme.dart';
Ian Hickson's avatar
Ian Hickson committed
32 33
import 'theme.dart';

34
/// A [ListTile] that shows an about box.
Ian Hickson's avatar
Ian Hickson committed
35
///
36 37
/// This widget is often added to an app's [Drawer]. When tapped it shows
/// an about box dialog with [showAboutDialog].
Ian Hickson's avatar
Ian Hickson committed
38 39
///
/// The about box will include a button that shows licenses for software used by
40 41
/// the application. The licenses shown are those returned by the
/// [LicenseRegistry] API, which can be used to add more licenses to the list.
Ian Hickson's avatar
Ian Hickson committed
42 43 44
///
/// If your application does not have a [Drawer], you should provide an
/// affordance to call [showAboutDialog] or (at least) [showLicensePage].
45
///
46
/// {@tool dartpad --template=stateless_widget_material}
47 48 49 50 51 52
///
/// This sample shows two ways to open [AboutDialog]. The first one
/// uses an [AboutListTile], and the second uses the [showAboutDialog] function.
///
/// ```dart
///  Widget build(BuildContext context) {
53 54
///    final ThemeData theme = Theme.of(context);
///    final TextStyle textStyle = theme.textTheme.bodyText2!;
55
///    final List<Widget> aboutBoxChildren = <Widget>[
56
///      const SizedBox(height: 24),
57 58 59 60 61
///      RichText(
///        text: TextSpan(
///          children: <TextSpan>[
///            TextSpan(
///              style: textStyle,
62
///              text: "Flutter is Google's UI toolkit for building beautiful, "
63 64 65 66
///              'natively compiled applications for mobile, web, and desktop '
///              'from a single codebase. Learn more about Flutter at '
///            ),
///            TextSpan(
67
///              style: textStyle.copyWith(color: theme.colorScheme.primary),
68 69 70 71 72 73 74 75 76 77 78 79 80
///              text: 'https://flutter.dev'
///            ),
///            TextSpan(
///              style: textStyle,
///              text: '.'
///            ),
///          ],
///        ),
///      ),
///    ];
///
///    return Scaffold(
///      appBar: AppBar(
81
///        title: const Text('Show About Example'),
82 83 84 85 86
///      ),
///      drawer: Drawer(
///        child: SingleChildScrollView(
///          child: SafeArea(
///            child: AboutListTile(
87 88
///              icon: const Icon(Icons.info),
///              applicationIcon: const FlutterLogo(),
89 90
///              applicationName: 'Show About Example',
///              applicationVersion: 'August 2019',
91
///              applicationLegalese: '\u{a9} 2014 The Flutter Authors',
92 93 94 95 96 97
///              aboutBoxChildren: aboutBoxChildren,
///            ),
///          ),
///        ),
///      ),
///      body: Center(
98
///        child: ElevatedButton(
99
///          child: const Text('Show About Example'),
100 101 102
///          onPressed: () {
///            showAboutDialog(
///              context: context,
103
///              applicationIcon: const FlutterLogo(),
104 105
///              applicationName: 'Show About Example',
///              applicationVersion: 'August 2019',
106
///              applicationLegalese: '\u{a9} 2014 The Flutter Authors',
107 108 109 110 111 112
///              children: aboutBoxChildren,
///            );
///          },
///        ),
///      ),
///    );
113
/// }
114 115
/// ```
/// {@end-tool}
116 117
class AboutListTile extends StatelessWidget {
  /// Creates a list tile for showing an about box.
Ian Hickson's avatar
Ian Hickson committed
118 119 120 121
  ///
  /// The arguments are all optional. The application name, if omitted, will be
  /// derived from the nearest [Title] widget. The version, icon, and legalese
  /// values default to the empty string.
122
  const AboutListTile({
123
    Key? key,
124
    this.icon,
Ian Hickson's avatar
Ian Hickson committed
125 126 127 128 129
    this.child,
    this.applicationName,
    this.applicationVersion,
    this.applicationIcon,
    this.applicationLegalese,
130
    this.aboutBoxChildren,
131
    this.dense,
Ian Hickson's avatar
Ian Hickson committed
132 133 134 135 136 137 138 139
  }) : super(key: key);

  /// The icon to show for this drawer item.
  ///
  /// By default no icon is shown.
  ///
  /// This is not necessarily the same as the image shown in the dialog box
  /// itself; which is controlled by the [applicationIcon] property.
140
  final Widget? icon;
Ian Hickson's avatar
Ian Hickson committed
141 142 143 144 145

  /// The label to show on this drawer item.
  ///
  /// Defaults to a text widget that says "About Foo" where "Foo" is the
  /// application name specified by [applicationName].
146
  final Widget? child;
Ian Hickson's avatar
Ian Hickson committed
147 148 149 150 151 152 153

  /// The name of the application.
  ///
  /// This string is used in the default label for this drawer item (see
  /// [child]) and as the caption of the [AboutDialog] that is shown.
  ///
  /// Defaults to the value of [Title.title], if a [Title] widget can be found.
154
  /// Otherwise, defaults to [Platform.resolvedExecutable].
155
  final String? applicationName;
Ian Hickson's avatar
Ian Hickson committed
156 157 158 159 160 161

  /// The version of this build of the application.
  ///
  /// This string is shown under the application name in the [AboutDialog].
  ///
  /// Defaults to the empty string.
162
  final String? applicationVersion;
Ian Hickson's avatar
Ian Hickson committed
163 164 165 166 167

  /// The icon to show next to the application name in the [AboutDialog].
  ///
  /// By default no icon is shown.
  ///
168 169 170
  /// Typically this will be an [ImageIcon] widget. It should honor the
  /// [IconTheme]'s [IconThemeData.size].
  ///
Ian Hickson's avatar
Ian Hickson committed
171 172
  /// This is not necessarily the same as the icon shown on the drawer item
  /// itself, which is controlled by the [icon] property.
173
  final Widget? applicationIcon;
Ian Hickson's avatar
Ian Hickson committed
174 175 176 177 178 179

  /// A string to show in small print in the [AboutDialog].
  ///
  /// Typically this is a copyright notice.
  ///
  /// Defaults to the empty string.
180
  final String? applicationLegalese;
Ian Hickson's avatar
Ian Hickson committed
181 182 183 184 185 186 187

  /// Widgets to add to the [AboutDialog] after the name, version, and legalese.
  ///
  /// This could include a link to a Web site, some descriptive text, credits,
  /// or other information to show in the about box.
  ///
  /// Defaults to nothing.
188
  final List<Widget>? aboutBoxChildren;
Ian Hickson's avatar
Ian Hickson committed
189

190 191 192 193 194
  /// Whether this list tile is part of a vertically dense list.
  ///
  /// If this property is null, then its value is based on [ListTileTheme.dense].
  ///
  /// Dense list tiles default to a smaller height.
195
  final bool? dense;
196

Ian Hickson's avatar
Ian Hickson committed
197 198 199
  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterial(context));
200
    assert(debugCheckHasMaterialLocalizations(context));
201
    return ListTile(
202
      leading: icon,
203
      title: child ?? Text(MaterialLocalizations.of(context).aboutListTileTitle(
204 205 206
        applicationName ?? _defaultApplicationName(context),
      )),
      dense: dense,
207
      onTap: () {
Ian Hickson's avatar
Ian Hickson committed
208 209 210 211 212 213
        showAboutDialog(
          context: context,
          applicationName: applicationName,
          applicationVersion: applicationVersion,
          applicationIcon: applicationIcon,
          applicationLegalese: applicationLegalese,
214
          children: aboutBoxChildren,
Ian Hickson's avatar
Ian Hickson committed
215
        );
216
      },
Ian Hickson's avatar
Ian Hickson committed
217 218 219 220 221 222 223 224 225
    );
  }
}

/// Displays an [AboutDialog], which describes the application and provides a
/// button to show licenses for software used by the application.
///
/// The arguments correspond to the properties on [AboutDialog].
///
226
/// If the application has a [Drawer], consider using [AboutListTile] instead
Ian Hickson's avatar
Ian Hickson committed
227 228 229 230
/// of calling this directly.
///
/// If you do not need an about box in your application, you should at least
/// provide an affordance to call [showLicensePage].
231 232 233
///
/// The licenses shown on the [LicensePage] are those returned by the
/// [LicenseRegistry] API, which can be used to add more licenses to the list.
Ian Hickson's avatar
Ian Hickson committed
234
///
235 236
/// The [context], [useRootNavigator] and [routeSettings] arguments are passed to
/// [showDialog], the documentation for which discusses how it is used.
Ian Hickson's avatar
Ian Hickson committed
237
void showAboutDialog({
238 239 240 241 242 243
  required BuildContext context,
  String? applicationName,
  String? applicationVersion,
  Widget? applicationIcon,
  String? applicationLegalese,
  List<Widget>? children,
244
  bool useRootNavigator = true,
245
  RouteSettings? routeSettings,
Ian Hickson's avatar
Ian Hickson committed
246
}) {
247
  assert(context != null);
248
  assert(useRootNavigator != null);
249
  showDialog<void>(
Ian Hickson's avatar
Ian Hickson committed
250
    context: context,
251
    useRootNavigator: useRootNavigator,
252
    builder: (BuildContext context) {
253
      return AboutDialog(
254 255 256 257 258 259
        applicationName: applicationName,
        applicationVersion: applicationVersion,
        applicationIcon: applicationIcon,
        applicationLegalese: applicationLegalese,
        children: children,
      );
260
    },
261
    routeSettings: routeSettings,
Ian Hickson's avatar
Ian Hickson committed
262 263 264 265 266 267
  );
}

/// Displays a [LicensePage], which shows licenses for software used by the
/// application.
///
268 269 270 271 272 273 274
/// The application arguments correspond to the properties on [LicensePage].
///
/// The `context` argument is used to look up the [Navigator] for the page.
///
/// The `useRootNavigator` argument is used to determine whether to push the
/// page to the [Navigator] furthest from or nearest to the given `context`. It
/// is `false` by default.
Ian Hickson's avatar
Ian Hickson committed
275
///
276
/// If the application has a [Drawer], consider using [AboutListTile] instead
Ian Hickson's avatar
Ian Hickson committed
277 278 279 280
/// of calling this directly.
///
/// The [AboutDialog] shown by [showAboutDialog] includes a button that calls
/// [showLicensePage].
281 282 283
///
/// The licenses shown on the [LicensePage] are those returned by the
/// [LicenseRegistry] API, which can be used to add more licenses to the list.
Ian Hickson's avatar
Ian Hickson committed
284
void showLicensePage({
285 286 287 288 289
  required BuildContext context,
  String? applicationName,
  String? applicationVersion,
  Widget? applicationIcon,
  String? applicationLegalese,
290
  bool useRootNavigator = false,
Ian Hickson's avatar
Ian Hickson committed
291
}) {
292
  assert(context != null);
293
  assert(useRootNavigator != null);
294
  Navigator.of(context, rootNavigator: useRootNavigator).push(MaterialPageRoute<void>(
295
    builder: (BuildContext context) => LicensePage(
296 297
      applicationName: applicationName,
      applicationVersion: applicationVersion,
298
      applicationIcon: applicationIcon,
299
      applicationLegalese: applicationLegalese,
300
    ),
301
  ));
Ian Hickson's avatar
Ian Hickson committed
302 303
}

304 305 306
/// The amount of vertical space to separate chunks of text.
const double _textVerticalSeparation = 18.0;

Ian Hickson's avatar
Ian Hickson committed
307 308 309 310 311
/// An about box. This is a dialog box with the application's icon, name,
/// version number, and copyright, plus a button to show licenses for software
/// used by the application.
///
/// To show an [AboutDialog], use [showAboutDialog].
312
///
313 314
/// {@youtube 560 315 https://www.youtube.com/watch?v=YFCSODyFxbE}
///
315
/// If the application has a [Drawer], the [AboutListTile] widget can make the
316 317 318 319 320 321 322
/// process of showing an about dialog simpler.
///
/// The [AboutDialog] shown by [showAboutDialog] includes a button that calls
/// [showLicensePage].
///
/// The licenses shown on the [LicensePage] are those returned by the
/// [LicenseRegistry] API, which can be used to add more licenses to the list.
Ian Hickson's avatar
Ian Hickson committed
323 324 325 326 327 328
class AboutDialog extends StatelessWidget {
  /// Creates an about box.
  ///
  /// The arguments are all optional. The application name, if omitted, will be
  /// derived from the nearest [Title] widget. The version, icon, and legalese
  /// values default to the empty string.
329
  const AboutDialog({
330
    Key? key,
Ian Hickson's avatar
Ian Hickson committed
331 332 333 334
    this.applicationName,
    this.applicationVersion,
    this.applicationIcon,
    this.applicationLegalese,
335
    this.children,
Ian Hickson's avatar
Ian Hickson committed
336 337 338 339 340
  }) : super(key: key);

  /// The name of the application.
  ///
  /// Defaults to the value of [Title.title], if a [Title] widget can be found.
341
  /// Otherwise, defaults to [Platform.resolvedExecutable].
342
  final String? applicationName;
Ian Hickson's avatar
Ian Hickson committed
343 344 345 346 347 348

  /// The version of this build of the application.
  ///
  /// This string is shown under the application name.
  ///
  /// Defaults to the empty string.
349
  final String? applicationVersion;
Ian Hickson's avatar
Ian Hickson committed
350 351 352 353

  /// The icon to show next to the application name.
  ///
  /// By default no icon is shown.
354 355 356
  ///
  /// Typically this will be an [ImageIcon] widget. It should honor the
  /// [IconTheme]'s [IconThemeData.size].
357
  final Widget? applicationIcon;
Ian Hickson's avatar
Ian Hickson committed
358 359 360 361 362 363

  /// A string to show in small print.
  ///
  /// Typically this is a copyright notice.
  ///
  /// Defaults to the empty string.
364
  final String? applicationLegalese;
Ian Hickson's avatar
Ian Hickson committed
365 366 367 368 369 370 371

  /// Widgets to add to the dialog box after the name, version, and legalese.
  ///
  /// This could include a link to a Web site, some descriptive text, credits,
  /// or other information to show in the about box.
  ///
  /// Defaults to nothing.
372
  final List<Widget>? children;
Ian Hickson's avatar
Ian Hickson committed
373 374 375

  @override
  Widget build(BuildContext context) {
376
    assert(debugCheckHasMaterialLocalizations(context));
Ian Hickson's avatar
Ian Hickson committed
377 378
    final String name = applicationName ?? _defaultApplicationName(context);
    final String version = applicationVersion ?? _defaultApplicationVersion(context);
379
    final Widget? icon = applicationIcon ?? _defaultApplicationIcon(context);
380
    return AlertDialog(
381 382 383 384 385
      content: ListBody(
        children: <Widget>[
          Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
386
              if (icon != null) IconTheme(data: Theme.of(context).iconTheme, child: icon),
387 388 389 390 391
              Expanded(
                child: Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 24.0),
                  child: ListBody(
                    children: <Widget>[
392 393
                      Text(name, style: Theme.of(context).textTheme.headline5),
                      Text(version, style: Theme.of(context).textTheme.bodyText2),
394
                      const SizedBox(height: _textVerticalSeparation),
395
                      Text(applicationLegalese ?? '', style: Theme.of(context).textTheme.caption),
396
                    ],
397 398
                  ),
                ),
399 400 401 402 403
              ),
            ],
          ),
          ...?children,
        ],
404
      ),
Ian Hickson's avatar
Ian Hickson committed
405
      actions: <Widget>[
406
        TextButton(
407
          child: Text(MaterialLocalizations.of(context).viewLicensesButtonLabel),
Ian Hickson's avatar
Ian Hickson committed
408 409 410 411 412 413
          onPressed: () {
            showLicensePage(
              context: context,
              applicationName: applicationName,
              applicationVersion: applicationVersion,
              applicationIcon: applicationIcon,
414
              applicationLegalese: applicationLegalese,
Ian Hickson's avatar
Ian Hickson committed
415
            );
416
          },
Ian Hickson's avatar
Ian Hickson committed
417
        ),
418
        TextButton(
419
          child: Text(MaterialLocalizations.of(context).closeButtonLabel),
Ian Hickson's avatar
Ian Hickson committed
420 421
          onPressed: () {
            Navigator.pop(context);
422
          },
Ian Hickson's avatar
Ian Hickson committed
423
        ),
424
      ],
425
      scrollable: true,
Ian Hickson's avatar
Ian Hickson committed
426 427 428 429 430 431 432
    );
  }
}

/// A page that shows licenses for software used by the application.
///
/// To show a [LicensePage], use [showLicensePage].
433
///
434
/// The [AboutDialog] shown by [showAboutDialog] and [AboutListTile] includes
435 436 437 438
/// a button that calls [showLicensePage].
///
/// The licenses shown on the [LicensePage] are those returned by the
/// [LicenseRegistry] API, which can be used to add more licenses to the list.
439
class LicensePage extends StatefulWidget {
Ian Hickson's avatar
Ian Hickson committed
440 441 442 443 444
  /// Creates a page that shows licenses for software used by the application.
  ///
  /// The arguments are all optional. The application name, if omitted, will be
  /// derived from the nearest [Title] widget. The version and legalese values
  /// default to the empty string.
445 446 447
  ///
  /// The licenses shown on the [LicensePage] are those returned by the
  /// [LicenseRegistry] API, which can be used to add more licenses to the list.
Ian Hickson's avatar
Ian Hickson committed
448
  const LicensePage({
449
    Key? key,
Ian Hickson's avatar
Ian Hickson committed
450 451
    this.applicationName,
    this.applicationVersion,
452
    this.applicationIcon,
453
    this.applicationLegalese,
Ian Hickson's avatar
Ian Hickson committed
454 455 456 457 458
  }) : super(key: key);

  /// The name of the application.
  ///
  /// Defaults to the value of [Title.title], if a [Title] widget can be found.
459
  /// Otherwise, defaults to [Platform.resolvedExecutable].
460
  final String? applicationName;
Ian Hickson's avatar
Ian Hickson committed
461 462 463 464 465 466

  /// The version of this build of the application.
  ///
  /// This string is shown under the application name.
  ///
  /// Defaults to the empty string.
467
  final String? applicationVersion;
Ian Hickson's avatar
Ian Hickson committed
468

469 470 471 472 473 474
  /// The icon to show below the application name.
  ///
  /// By default no icon is shown.
  ///
  /// Typically this will be an [ImageIcon] widget. It should honor the
  /// [IconTheme]'s [IconThemeData.size].
475
  final Widget? applicationIcon;
476

Ian Hickson's avatar
Ian Hickson committed
477 478 479 480 481
  /// A string to show in small print.
  ///
  /// Typically this is a copyright notice.
  ///
  /// Defaults to the empty string.
482
  final String? applicationLegalese;
Ian Hickson's avatar
Ian Hickson committed
483

484
  @override
485
  _LicensePageState createState() => _LicensePageState();
486 487 488
}

class _LicensePageState extends State<LicensePage> {
489
  final ValueNotifier<int?> selectedId = ValueNotifier<int?>(null);
490 491 492 493 494

  @override
  Widget build(BuildContext context) {
    return _MasterDetailFlow(
      detailPageFABlessGutterWidth: _getGutterSize(context),
495
      title: Text(MaterialLocalizations.of(context).licensesPageTitle),
496 497 498 499 500
      detailPageBuilder: _packageLicensePage,
      masterViewBuilder: _packagesView,
    );
  }

501
  Widget _packageLicensePage(BuildContext _, Object? args, ScrollController? scrollController) {
502
    assert(args is _DetailArguments);
503
    final _DetailArguments detailArguments = args! as _DetailArguments;
504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527
    return _PackageLicensePage(
      packageName: detailArguments.packageName,
      licenseEntries: detailArguments.licenseEntries,
      scrollController: scrollController,
    );
  }

  Widget _packagesView(final BuildContext _, final bool isLateral) {
    final Widget about = _AboutProgram(
        name: widget.applicationName ?? _defaultApplicationName(context),
        icon: widget.applicationIcon ?? _defaultApplicationIcon(context),
        version: widget.applicationVersion ?? _defaultApplicationVersion(context),
        legalese: widget.applicationLegalese,
      );
    return _PackagesView(
      about: about,
      isLateral: isLateral,
      selectedId: selectedId,
    );
  }
}

class _AboutProgram extends StatelessWidget {
  const _AboutProgram({
528 529 530
    Key? key,
    required this.name,
    required this.version,
531 532 533 534 535 536 537 538
    this.icon,
    this.legalese,
  })  : assert(name != null),
        assert(version != null),
        super(key: key);

  final String name;
  final String version;
539 540
  final Widget? icon;
  final String? legalese;
541 542 543 544 545 546 547 548 549 550 551 552

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.symmetric(
        horizontal: _getGutterSize(context),
        vertical: 24.0,
      ),
      child: Column(
        children: <Widget>[
          Text(
            name,
553
            style: Theme.of(context).textTheme.headline5,
554 555 556
            textAlign: TextAlign.center,
          ),
          if (icon != null)
557
            IconTheme(data: Theme.of(context).iconTheme, child: icon!),
558 559
          Text(
            version,
560
            style: Theme.of(context).textTheme.bodyText2,
561 562 563 564 565
            textAlign: TextAlign.center,
          ),
          const SizedBox(height: _textVerticalSeparation),
          Text(
            legalese ?? '',
566
            style: Theme.of(context).textTheme.caption,
567 568 569 570 571
            textAlign: TextAlign.center,
          ),
          const SizedBox(height: _textVerticalSeparation),
          Text(
            'Powered by Flutter',
572
            style: Theme.of(context).textTheme.bodyText2,
573 574 575 576 577 578 579 580 581 582
            textAlign: TextAlign.center,
          ),
        ],
      ),
    );
  }
}

class _PackagesView extends StatefulWidget {
  const _PackagesView({
583 584 585 586
    Key? key,
    required this.about,
    required this.isLateral,
    required this.selectedId,
587 588 589 590 591 592
  })  : assert(about != null),
        assert(isLateral != null),
        super(key: key);

  final Widget about;
  final bool isLateral;
593
  final ValueNotifier<int?> selectedId;
594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611

  @override
  _PackagesViewState createState() => _PackagesViewState();
}

class _PackagesViewState extends State<_PackagesView> {
  final Future<_LicenseData> licenses = LicenseRegistry.licenses
      .fold<_LicenseData>(
        _LicenseData(),
        (_LicenseData prev, LicenseEntry license) => prev..addLicense(license),
      )
      .then((_LicenseData licenseData) => licenseData..sortPackages());

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<_LicenseData>(
      future: licenses,
      builder: (BuildContext context, AsyncSnapshot<_LicenseData> snapshot) {
612 613 614 615 616
        return LayoutBuilder(
          key: ValueKey<ConnectionState>(snapshot.connectionState),
          builder: (BuildContext context, BoxConstraints constraints) {
            switch (snapshot.connectionState) {
              case ConnectionState.done:
617 618
                _initDefaultDetailPage(snapshot.data!, context);
                return ValueListenableBuilder<int?>(
619
                  valueListenable: widget.selectedId,
620
                  builder: (BuildContext context, int? selectedId, Widget? _) {
621 622
                    return Center(
                      child: Material(
623
                        color: Theme.of(context).cardColor,
624 625 626
                        elevation: 4.0,
                        child: Container(
                          constraints: BoxConstraints.loose(const Size.fromWidth(600.0)),
627
                          child: _packagesList(context, selectedId, snapshot.data!, widget.isLateral),
628
                        ),
629 630 631 632 633 634
                      ),
                    );
                  },
                );
              default:
                return Material(
635
                    color: Theme.of(context).cardColor,
636
                    child: Column(
637 638 639 640 641
                    mainAxisAlignment: MainAxisAlignment.start,
                    children: <Widget>[
                      widget.about,
                      const Center(child: CircularProgressIndicator()),
                    ],
642 643 644 645
                  ),
                );
            }
          },
646 647 648 649 650 651
        );
      },
    );
  }

  void _initDefaultDetailPage(_LicenseData data, BuildContext context) {
652 653 654
    if (data.packages.isEmpty) {
      return;
    }
655
    final String packageName = data.packages[widget.selectedId.value ?? 0];
656 657
    final List<int> bindings = data.packageLicenseBindings[packageName]!;
    _MasterDetailFlow.of(context)!.setInitialDetailPage(
658 659 660 661 662 663 664 665 666
      _DetailArguments(
        packageName,
        bindings.map((int i) => data.licenses[i]).toList(growable: false),
      ),
    );
  }

  Widget _packagesList(
    final BuildContext context,
667
    final int? selectedId,
668 669 670 671 672 673 674 675 676 677 678 679
    final _LicenseData data,
    final bool drawSelection,
  ) {
    return ListView(
      children: <Widget>[
        widget.about,
        ...data.packages
            .asMap()
            .entries
            .map<Widget>((MapEntry<int, String> entry) {
          final String packageName = entry.value;
          final int index = entry.key;
680
          final List<int> bindings = data.packageLicenseBindings[packageName]!;
681 682 683 684 685 686 687
          return _PackageListTile(
            packageName: packageName,
            index: index,
            isSelected: drawSelection && entry.key == (selectedId ?? 0),
            numberLicenses: bindings.length,
            onTap: () {
              widget.selectedId.value = index;
688
              _MasterDetailFlow.of(context)!.openDetailPage(_DetailArguments(
689 690 691 692 693 694 695 696 697 698 699 700 701
                packageName,
                bindings.map((int i) => data.licenses[i]).toList(growable: false),
              ));
            },
          );
        }),
      ],
    );
  }
}

class _PackageListTile extends StatelessWidget {
  const _PackageListTile({
702 703
    Key? key,
    required this.packageName,
704
    this.index,
705 706
    required this.isSelected,
    required this.numberLicenses,
707 708 709 710
    this.onTap,
}) : super(key:key);

  final String packageName;
711
  final int? index;
712 713
  final bool isSelected;
  final int numberLicenses;
714
  final GestureTapCallback? onTap;
715 716 717 718

  @override
  Widget build(BuildContext context) {
    return Ink(
719
      color: isSelected ? Theme.of(context).highlightColor : Theme.of(context).cardColor,
720 721
      child: ListTile(
        title: Text(packageName),
722
        subtitle: Text(MaterialLocalizations.of(context).licensesPackageDetailText(numberLicenses)),
723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739
        selected: isSelected,
        onTap: onTap,
      ),
    );
  }
}

/// This is a collection of licenses and the packages to which they apply.
/// [packageLicenseBindings] records the m+:n+ relationship between the license
/// and packages as a map of package names to license indexes.
class _LicenseData {
  final List<LicenseEntry> licenses = <LicenseEntry>[];
  final Map<String, List<int>> packageLicenseBindings = <String, List<int>>{};
  final List<String> packages = <String>[];

  // Special treatment for the first package since it should be the package
  // for delivered application.
740
  String? firstPackage;
741 742 743 744 745 746 747 748 749

  void addLicense(LicenseEntry entry) {
    // Before the license can be added, we must first record the packages to
    // which it belongs.
    for (final String package in entry.packages) {
      _addPackage(package);
      // Bind this license to the package using the next index value. This
      // creates a contract that this license must be inserted at this same
      // index value.
750
      packageLicenseBindings[package]!.add(licenses.length);
751 752 753 754
    }
    licenses.add(entry); // Completion of the contract above.
  }

755
  /// Add a package and initialize package license binding. This is a no-op if
756 757 758 759 760 761 762 763 764 765 766 767
  /// the package has been seen before.
  void _addPackage(String package) {
    if (!packageLicenseBindings.containsKey(package)) {
      packageLicenseBindings[package] = <int>[];
      firstPackage ??= package;
      packages.add(package);
    }
  }

  /// Sort the packages using some comparison method, or by the default manner,
  /// which is to put the application package first, followed by every other
  /// package in case-insensitive alphabetical order.
768
  void sortPackages([int Function(String a, String b)? compare]) {
769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805
    packages.sort(compare ?? (String a, String b) {
      // Based on how LicenseRegistry currently behaves, the first package
      // returned is the end user application license. This should be
      // presented first in the list. So here we make sure that first package
      // remains at the front regardless of alphabetical sorting.
      if (a == firstPackage) {
        return -1;
      }
      if (b == firstPackage) {
        return 1;
      }
      return a.toLowerCase().compareTo(b.toLowerCase());
    });
  }
}

@immutable
class _DetailArguments {
  const _DetailArguments(this.packageName, this.licenseEntries);

  final String packageName;
  final List<LicenseEntry> licenseEntries;

  @override
  bool operator ==(final dynamic other) {
    if (other is _DetailArguments) {
      return other.packageName == packageName;
    }
    return other == this;
  }

  @override
  int get hashCode => packageName.hashCode; // Good enough.
}

class _PackageLicensePage extends StatefulWidget {
  const _PackageLicensePage({
806 807 808 809
    Key? key,
    required this.packageName,
    required this.licenseEntries,
    required this.scrollController,
810 811 812 813
  }) : super(key: key);

  final String packageName;
  final List<LicenseEntry> licenseEntries;
814
  final ScrollController? scrollController;
815 816 817 818 819 820

  @override
  _PackageLicensePageState createState() => _PackageLicensePageState();
}

class _PackageLicensePageState extends State<_PackageLicensePage> {
Ian Hickson's avatar
Ian Hickson committed
821 822 823 824 825 826
  @override
  void initState() {
    super.initState();
    _initLicenses();
  }

827
  final List<Widget> _licenses = <Widget>[];
Ian Hickson's avatar
Ian Hickson committed
828 829
  bool _loaded = false;

830
  Future<void> _initLicenses() async {
831 832 833 834 835 836 837
    int debugFlowId = -1;
    assert(() {
      final Flow flow = Flow.begin();
      Timeline.timeSync('_initLicenses()', () { }, flow: flow);
      debugFlowId = flow.id;
      return true;
    }());
838
    for (final LicenseEntry license in widget.licenseEntries) {
839
      if (!mounted) {
840
        return;
841 842 843 844 845
      }
      assert(() {
        Timeline.timeSync('_initLicenses()', () { }, flow: Flow.step(debugFlowId));
        return true;
      }());
846
      final List<LicenseParagraph> paragraphs =
847
        await SchedulerBinding.instance!.scheduleTask<List<LicenseParagraph>>(
848
          license.paragraphs.toList,
849 850 851
          Priority.animation,
          debugLabel: 'License',
        );
852 853 854
      if (!mounted) {
        return;
      }
Ian Hickson's avatar
Ian Hickson committed
855
      setState(() {
856
        _licenses.add(const Padding(
857 858
          padding: EdgeInsets.all(18.0),
          child: Divider(),
Ian Hickson's avatar
Ian Hickson committed
859
        ));
860
        for (final LicenseParagraph paragraph in paragraphs) {
Ian Hickson's avatar
Ian Hickson committed
861
          if (paragraph.indent == LicenseParagraph.centeredIndent) {
862
            _licenses.add(Padding(
863
              padding: const EdgeInsets.only(top: 16.0),
864
              child: Text(
Ian Hickson's avatar
Ian Hickson committed
865
                paragraph.text,
866
                style: const TextStyle(fontWeight: FontWeight.bold),
867 868
                textAlign: TextAlign.center,
              ),
Ian Hickson's avatar
Ian Hickson committed
869 870 871
            ));
          } else {
            assert(paragraph.indent >= 0);
872 873
            _licenses.add(Padding(
              padding: EdgeInsetsDirectional.only(top: 8.0, start: 16.0 * paragraph.indent),
874
              child: Text(paragraph.text),
Ian Hickson's avatar
Ian Hickson committed
875 876
            ));
          }
877
        }
Ian Hickson's avatar
Ian Hickson committed
878
      });
879
    }
Ian Hickson's avatar
Ian Hickson committed
880 881 882
    setState(() {
      _loaded = true;
    });
883 884 885 886
    assert(() {
      Timeline.timeSync('Build scheduled', () { }, flow: Flow.end(debugFlowId));
      return true;
    }());
887 888
  }

Ian Hickson's avatar
Ian Hickson committed
889 890
  @override
  Widget build(BuildContext context) {
891
    assert(debugCheckHasMaterialLocalizations(context));
892
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
893
    final ThemeData theme = Theme.of(context);
894 895 896 897 898 899 900 901 902 903 904 905 906 907 908
    final String title = widget.packageName;
    final String subtitle = localizations.licensesPackageDetailText(widget.licenseEntries.length);
    final double pad = _getGutterSize(context);
    final EdgeInsets padding = EdgeInsets.only(left: pad, right: pad, bottom: pad);
    final List<Widget> listWidgets = <Widget>[
      ..._licenses,
      if (!_loaded)
        const Padding(
          padding: EdgeInsets.symmetric(vertical: 24.0),
          child: Center(
            child: CircularProgressIndicator(),
          ),
        ),
    ];

909
    final Widget page;
910 911 912
    if (widget.scrollController == null) {
      page = Scaffold(
        appBar: AppBar(
913 914 915
          title: _PackageLicensePageTitle(
            title,
            subtitle,
916
            theme.appBarTheme.textTheme ?? theme.primaryTextTheme,
917
          ),
918 919 920 921 922 923 924 925 926 927 928 929 930
        ),
        body: Center(
          child: Material(
            color: theme.cardColor,
            elevation: 4.0,
            child: Container(
              constraints: BoxConstraints.loose(const Size.fromWidth(600.0)),
              child: Localizations.override(
                locale: const Locale('en', 'US'),
                context: context,
                child: Scrollbar(
                  child: ListView(padding: padding, children: listWidgets),
                ),
931
              ),
Hans Muller's avatar
Hans Muller committed
932
            ),
933 934
          ),
        ),
935 936 937 938 939 940 941 942
      );
    } else {
      page = CustomScrollView(
        controller: widget.scrollController,
        slivers: <Widget>[
          SliverAppBar(
            automaticallyImplyLeading: false,
            pinned: true,
943
            backgroundColor: theme.cardColor,
944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962
            title: _PackageLicensePageTitle(title, subtitle, theme.textTheme),
          ),
          SliverPadding(
            padding: padding,
            sliver: SliverList(
              delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) => Localizations.override(
                  locale: const Locale('en', 'US'),
                  context: context,
                  child: listWidgets[index],
                ),
                childCount: listWidgets.length,
              ),
            ),
          ),
        ],
      );
    }
    return DefaultTextStyle(
963
      style: theme.textTheme.caption!,
964 965 966 967 968 969 970 971 972 973
      child: page,
    );
  }
}

class _PackageLicensePageTitle extends StatelessWidget {
  const _PackageLicensePageTitle(
    this.title,
    this.subtitle,
    this.theme, {
974
    Key? key,
975 976 977 978 979 980 981 982 983 984 985 986 987 988 989
  }) : super(key: key);

  final String title;
  final String subtitle;
  final TextTheme theme;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text(title, style: theme.headline6),
        Text(subtitle, style: theme.subtitle2),
      ],
Ian Hickson's avatar
Ian Hickson committed
990 991 992 993 994
    );
  }
}

String _defaultApplicationName(BuildContext context) {
995 996 997 998 999 1000
  // This doesn't handle the case of the application's title dynamically
  // changing. In theory, we should make Title expose the current application
  // title using an InheritedWidget, and so forth. However, in practice, if
  // someone really wants their application title to change dynamically, they
  // can provide an explicit applicationName to the widgets defined in this
  // file, instead of relying on the default.
1001
  final Title? ancestorTitle = context.findAncestorWidgetOfExactType<Title>();
1002
  return ancestorTitle?.title ?? Platform.resolvedExecutable.split(Platform.pathSeparator).last;
Ian Hickson's avatar
Ian Hickson committed
1003 1004 1005 1006 1007 1008 1009
}

String _defaultApplicationVersion(BuildContext context) {
  // TODO(ianh): Get this from the embedder somehow.
  return '';
}

1010
Widget? _defaultApplicationIcon(BuildContext context) {
Ian Hickson's avatar
Ian Hickson committed
1011 1012 1013
  // TODO(ianh): Get this from the embedder somehow.
  return null;
}
1014 1015 1016 1017 1018 1019

const int _materialGutterThreshold = 720;
const double _wideGutterSize = 24.0;
const double _narrowGutterSize = 12.0;

double _getGutterSize(BuildContext context) =>
1020
    MediaQuery.of(context).size.width >= _materialGutterThreshold ? _wideGutterSize : _narrowGutterSize;
1021 1022 1023 1024 1025 1026 1027 1028

/// Signature for the builder callback used by [_MasterDetailFlow].
typedef _MasterViewBuilder = Widget Function(BuildContext context, bool isLateralUI);

/// Signature for the builder callback used by [_MasterDetailFlow.detailPageBuilder].
///
/// scrollController is provided when the page destination is the draggable
/// sheet in the lateral UI. Otherwise, it is null.
1029
typedef _DetailPageBuilder = Widget Function(BuildContext context, Object? arguments, ScrollController? scrollController);
1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077

/// Signature for the builder callback used by [_MasterDetailFlow.actionBuilder].
///
/// Builds the actions that go in the app bars constructed for the master and
/// lateral UI pages. actionLevel indicates the intended destination of the
/// return actions.
typedef _ActionBuilder = List<Widget> Function(BuildContext context, _ActionLevel actionLevel);

/// Describes which type of app bar the actions are intended for.
enum _ActionLevel {
  /// Indicates the top app bar in the lateral UI.
  top,

  /// Indicates the master view app bar in the lateral UI.
  view,

  /// Indicates the master page app bar in the nested UI.
  composite,
}

/// Describes which layout will be used by [_MasterDetailFlow].
enum _LayoutMode {
  /// Use a nested or lateral layout depending on available screen width.
  auto,

  /// Always use a lateral layout.
  lateral,

  /// Always use a nested layout.
  nested,
}

const String _navMaster = 'master';
const String _navDetail = 'detail';
enum _Focus { master, detail }

/// A Master Detail Flow widget. Depending on screen width it builds either a
/// lateral or nested navigation flow between a master view and a detail page.
/// bloc pattern.
///
/// If focus is on detail view, then switching to nested navigation will
/// populate the navigation history with the master page and the detail page on
/// top. Otherwise the focus is on the master view and just the master page
/// is shown.
class _MasterDetailFlow extends StatefulWidget {
  /// Creates a master detail navigation flow which is either nested or
  /// lateral depending on screen width.
  const _MasterDetailFlow({
1078 1079 1080
    Key? key,
    required this.detailPageBuilder,
    required this.masterViewBuilder,
1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112
    this.actionBuilder,
    this.automaticallyImplyLeading = true,
    this.breakpoint,
    this.centerTitle,
    this.detailPageFABGutterWidth,
    this.detailPageFABlessGutterWidth,
    this.displayMode = _LayoutMode.auto,
    this.flexibleSpace,
    this.floatingActionButton,
    this.floatingActionButtonLocation,
    this.floatingActionButtonMasterPageLocation,
    this.leading,
    this.masterPageBuilder,
    this.masterViewWidth,
    this.title,
  })  : assert(masterViewBuilder != null),
        assert(automaticallyImplyLeading != null),
        assert(detailPageBuilder != null),
        assert(displayMode != null),
        super(key: key);

  /// Builder for the master view for lateral navigation.
  ///
  /// If [masterPageBuilder] is not supplied the master page required for nested navigation, also
  /// builds the master view inside a [Scaffold] with an [AppBar].
  final _MasterViewBuilder masterViewBuilder;

  /// Builder for the master page for nested navigation.
  ///
  /// This builder is usually a wrapper around the [masterViewBuilder] builder to provide the
  /// extra UI required to make a page. However, this builder is optional, and the master page
  /// can be built using the master view builder and the configuration for the lateral UI's app bar.
1113
  final _MasterViewBuilder? masterPageBuilder;
1114 1115 1116 1117 1118 1119 1120 1121 1122 1123

  /// Builder for the detail page.
  ///
  /// If scrollController == null, the page is intended for nested navigation. The lateral detail
  /// page is inside a [DraggableScrollableSheet] and should have a scrollable element that uses
  /// the [ScrollController] provided. In fact, it is strongly recommended the entire lateral
  /// page is scrollable.
  final _DetailPageBuilder detailPageBuilder;

  /// Override the width of the master view in the lateral UI.
1124
  final double? masterViewWidth;
1125 1126

  /// Override the width of the floating action button gutter in the lateral UI.
1127
  final double? detailPageFABGutterWidth;
1128 1129

  /// Override the width of the gutter when there is no floating action button.
1130
  final double? detailPageFABlessGutterWidth;
1131 1132 1133 1134 1135

  /// Add a floating action button to the lateral UI. If no [masterPageBuilder] is supplied, this
  /// floating action button is also used on the nested master page.
  ///
  /// See [Scaffold.floatingActionButton].
1136
  final FloatingActionButton? floatingActionButton;
1137 1138 1139 1140

  /// The title for the lateral UI [AppBar].
  ///
  /// See [AppBar.title].
1141
  final Widget? title;
1142 1143 1144 1145

  /// A widget to display before the title for the lateral UI [AppBar].
  ///
  /// See [AppBar.leading].
1146
  final Widget? leading;
1147 1148 1149 1150 1151 1152

  /// Override the framework from determining whether to show a leading widget or not.
  ///
  /// See [AppBar.automaticallyImplyLeading].
  final bool automaticallyImplyLeading;

1153
  /// Override the framework from determining whether to display the title in the center of the
1154 1155 1156
  /// app bar or not.
  ///
  /// See [AppBar.centerTitle].
1157
  final bool? centerTitle;
1158 1159

  /// See [AppBar.flexibleSpace].
1160
  final Widget? flexibleSpace;
1161 1162 1163 1164 1165 1166 1167 1168 1169 1170

  /// Build actions for the lateral UI, and potentially the master page in the nested UI.
  ///
  /// If level is [_ActionLevel.top] then the actions are for
  /// the entire lateral UI page. If level is [_ActionLevel.view] the actions
  /// are for the master
  /// view toolbar. Finally, if the [AppBar] for the master page for the nested UI is being built
  /// by [_MasterDetailFlow], then [_ActionLevel.composite] indicates the
  /// actions are for the
  /// nested master page.
1171
  final _ActionBuilder? actionBuilder;
1172 1173 1174 1175 1176 1177

  /// Determine where the floating action button will go.
  ///
  /// If null, [FloatingActionButtonLocation.endTop] is used.
  ///
  /// Also see [Scaffold.floatingActionButtonLocation].
1178
  final FloatingActionButtonLocation? floatingActionButtonLocation;
1179 1180 1181 1182

  /// Determine where the floating action button will go on the master page.
  ///
  /// See [Scaffold.floatingActionButtonLocation].
1183
  final FloatingActionButtonLocation? floatingActionButtonMasterPageLocation;
1184 1185 1186 1187 1188

  /// Forces display mode and style.
  final _LayoutMode displayMode;

  /// Width at which layout changes from nested to lateral.
1189
  final double? breakpoint;
1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201

  @override
  _MasterDetailFlowState createState() => _MasterDetailFlowState();

  /// The master detail flow proxy from the closest instance of this class that encloses the given
  /// context.
  ///
  /// Typical usage is as follows:
  ///
  /// ```dart
  /// _MasterDetailFlow.of(context).openDetailPage(arguments);
  /// ```
1202
  static _MasterDetailFlowProxy? of(BuildContext context) {
1203
    _PageOpener? pageOpener = context.findAncestorStateOfType<_MasterDetailScaffoldState>();
1204 1205
    pageOpener ??= context.findAncestorStateOfType<_MasterDetailFlowState>();
    assert(() {
1206
      if (pageOpener == null) {
1207
        throw FlutterError(
1208 1209
          'Master Detail operation requested with a context that does not include a Master Detail '
          'Flow.\nThe context used to open a detail page from the Master Detail Flow must be '
1210
          'that of a widget that is a descendant of a Master Detail Flow widget.',
1211
        );
1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245
      }
      return true;
    }());
    return pageOpener != null ? _MasterDetailFlowProxy._(pageOpener) : null;
  }
}

/// Interface for interacting with the [_MasterDetailFlow].
class _MasterDetailFlowProxy implements _PageOpener {
  _MasterDetailFlowProxy._(this._pageOpener);

  final _PageOpener _pageOpener;

  /// Open detail page with arguments.
  @override
  void openDetailPage(Object arguments) =>
      _pageOpener.openDetailPage(arguments);

  /// Set the initial page to be open for the lateral layout. This can be set at any time, but
  /// will have no effect after any calls to openDetailPage.
  @override
  void setInitialDetailPage(Object arguments) =>
      _pageOpener.setInitialDetailPage(arguments);
}

abstract class _PageOpener {
  void openDetailPage(Object arguments);

  void setInitialDetailPage(Object arguments);
}

const int _materialWideDisplayThreshold = 840;

class _MasterDetailFlowState extends State<_MasterDetailFlow> implements _PageOpener {
1246
  /// Tracks whether focus is on the detail or master views. Determines behavior when switching
1247 1248 1249 1250
  /// from lateral to nested navigation.
  _Focus focus = _Focus.master;

  /// Cache of arguments passed when opening a detail page. Used when rebuilding.
1251
  Object? _cachedDetailArguments;
1252 1253

  /// Record of the layout that was built.
1254
  _LayoutMode? _builtLayout;
1255 1256 1257 1258 1259 1260 1261 1262

  /// Key to access navigator in the nested layout.
  final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();

  @override
  void openDetailPage(Object arguments) {
    _cachedDetailArguments = arguments;
    if (_builtLayout == _LayoutMode.nested) {
1263
      _navigatorKey.currentState!.pushNamed(_navDetail, arguments: arguments);
1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281
    } else {
      focus = _Focus.detail;
    }
  }

  @override
  void setInitialDetailPage(Object arguments) {
    _cachedDetailArguments = arguments;
  }

  @override
  Widget build(BuildContext context) {
    switch (widget.displayMode) {
      case _LayoutMode.nested:
        return _nestedUI(context);
      case _LayoutMode.lateral:
        return _lateralUI(context);
      case _LayoutMode.auto:
1282 1283 1284 1285 1286 1287 1288 1289
        return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
          final double availableWidth = constraints.maxWidth;
          if (availableWidth >= (widget.breakpoint ?? _materialWideDisplayThreshold)) {
            return _lateralUI(context);
          } else {
            return _nestedUI(context);
          }
        });
1290 1291 1292 1293 1294 1295 1296 1297 1298
    }
  }

  Widget _nestedUI(BuildContext context) {
    _builtLayout = _LayoutMode.nested;
    final MaterialPageRoute<void> masterPageRoute = _masterPageRoute(context);

    return WillPopScope(
      // Push pop check into nested navigator.
1299
      onWillPop: () async => !(await _navigatorKey.currentState!.maybePop()),
1300 1301 1302 1303 1304 1305 1306 1307 1308 1309
      child: Navigator(
        key: _navigatorKey,
        initialRoute: 'initial',
        onGenerateInitialRoutes: (NavigatorState navigator, String initialRoute) {
          switch (focus) {
            case _Focus.master:
              return <Route<void>>[masterPageRoute];
            case _Focus.detail:
              return <Route<void>>[
                masterPageRoute,
1310
                _detailPageRoute(_cachedDetailArguments),
1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337
              ];
          }
        },
        onGenerateRoute: (RouteSettings settings) {
          switch (settings.name) {
            case _navMaster:
              // Matching state to navigation event.
              focus = _Focus.master;
              return masterPageRoute;
            case _navDetail:
              // Matching state to navigation event.
              focus = _Focus.detail;
              // Cache detail page settings.
              _cachedDetailArguments = settings.arguments;
              return _detailPageRoute(_cachedDetailArguments);
            default:
              throw Exception('Unknown route ${settings.name}');
          }
        },
      ),
    );
  }

  MaterialPageRoute<void> _masterPageRoute(BuildContext context) {
    return MaterialPageRoute<dynamic>(
      builder: (BuildContext c) => BlockSemantics(
        child: widget.masterPageBuilder != null
1338
            ? widget.masterPageBuilder!(c, false)
1339 1340
            : _MasterPage(
                leading: widget.leading ??
1341 1342
                    (widget.automaticallyImplyLeading && Navigator.of(context).canPop()
                        ? BackButton(onPressed: () => Navigator.of(context).pop())
1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356
                        : null),
                title: widget.title,
                centerTitle: widget.centerTitle,
                flexibleSpace: widget.flexibleSpace,
                automaticallyImplyLeading: widget.automaticallyImplyLeading,
                floatingActionButton: widget.floatingActionButton,
                floatingActionButtonLocation: widget.floatingActionButtonMasterPageLocation,
                masterViewBuilder: widget.masterViewBuilder,
                actionBuilder: widget.actionBuilder,
              ),
      ),
    );
  }

1357
  MaterialPageRoute<void> _detailPageRoute(Object? arguments) {
1358 1359 1360 1361 1362
    return MaterialPageRoute<dynamic>(builder: (BuildContext context) {
      return WillPopScope(
        onWillPop: () async {
          // No need for setState() as rebuild happens on navigation pop.
          focus = _Focus.master;
1363
          Navigator.of(context).pop();
1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376
          return false;
        },
        child: BlockSemantics(child: widget.detailPageBuilder(context, arguments, null)),
      );
    });
  }

  Widget _lateralUI(BuildContext context) {
    _builtLayout = _LayoutMode.lateral;
    return _MasterDetailScaffold(
      actionBuilder: widget.actionBuilder ?? (_, __) => const<Widget>[],
      automaticallyImplyLeading: widget.automaticallyImplyLeading,
      centerTitle: widget.centerTitle,
1377
      detailPageBuilder: (BuildContext context, Object? args, ScrollController? scrollController) =>
1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393
          widget.detailPageBuilder(context, args ?? _cachedDetailArguments, scrollController),
      floatingActionButton: widget.floatingActionButton,
      detailPageFABlessGutterWidth: widget.detailPageFABlessGutterWidth,
      detailPageFABGutterWidth: widget.detailPageFABGutterWidth,
      floatingActionButtonLocation: widget.floatingActionButtonLocation,
      initialArguments: _cachedDetailArguments,
      leading: widget.leading,
      masterViewBuilder: (BuildContext context, bool isLateral) => widget.masterViewBuilder(context, isLateral),
      masterViewWidth: widget.masterViewWidth,
      title: widget.title,
    );
  }
}

class _MasterPage extends StatelessWidget {
  const _MasterPage({
1394
    Key? key,
1395 1396 1397 1398 1399 1400 1401 1402
    this.leading,
    this.title,
    this.actionBuilder,
    this.centerTitle,
    this.flexibleSpace,
    this.floatingActionButton,
    this.floatingActionButtonLocation,
    this.masterViewBuilder,
1403
    required this.automaticallyImplyLeading,
1404 1405
  }) : super(key: key);

1406 1407 1408
  final _MasterViewBuilder? masterViewBuilder;
  final Widget? title;
  final Widget? leading;
1409
  final bool automaticallyImplyLeading;
1410 1411 1412 1413 1414
  final bool? centerTitle;
  final Widget? flexibleSpace;
  final _ActionBuilder? actionBuilder;
  final FloatingActionButton? floatingActionButton;
  final FloatingActionButtonLocation? floatingActionButtonLocation;
1415 1416 1417 1418 1419 1420 1421 1422 1423

  @override
  Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: title,
          leading: leading,
          actions: actionBuilder == null
              ? const <Widget>[]
1424
              : actionBuilder!(context, _ActionLevel.composite),
1425 1426 1427 1428
          centerTitle: centerTitle,
          flexibleSpace: flexibleSpace,
          automaticallyImplyLeading: automaticallyImplyLeading,
        ),
1429
        body: masterViewBuilder!(context, false),
1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443
        floatingActionButton: floatingActionButton,
        floatingActionButtonLocation: floatingActionButtonLocation,
      );
  }

}

const double _kCardElevation = 4.0;
const double _kMasterViewWidth = 320.0;
const double _kDetailPageFABlessGutterWidth = 40.0;
const double _kDetailPageFABGutterWidth = 84.0;

class _MasterDetailScaffold extends StatefulWidget {
  const _MasterDetailScaffold({
1444 1445 1446
    Key? key,
    required this.detailPageBuilder,
    required this.masterViewBuilder,
1447 1448 1449 1450 1451 1452
    this.actionBuilder,
    this.floatingActionButton,
    this.floatingActionButtonLocation,
    this.initialArguments,
    this.leading,
    this.title,
1453
    required this.automaticallyImplyLeading,
1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469
    this.centerTitle,
    this.detailPageFABlessGutterWidth,
    this.detailPageFABGutterWidth,
    this.masterViewWidth,
  })  : assert(detailPageBuilder != null),
        assert(masterViewBuilder != null),
        super(key: key);

  final _MasterViewBuilder masterViewBuilder;

  /// Builder for the detail page.
  ///
  /// The detail page is inside a [DraggableScrollableSheet] and should have a scrollable element
  /// that uses the [ScrollController] provided. In fact, it is strongly recommended the entire
  /// lateral page is scrollable.
  final _DetailPageBuilder detailPageBuilder;
1470 1471 1472 1473 1474 1475
  final _ActionBuilder? actionBuilder;
  final FloatingActionButton? floatingActionButton;
  final FloatingActionButtonLocation? floatingActionButtonLocation;
  final Object? initialArguments;
  final Widget? leading;
  final Widget? title;
1476
  final bool automaticallyImplyLeading;
1477 1478 1479 1480
  final bool? centerTitle;
  final double? detailPageFABlessGutterWidth;
  final double? detailPageFABGutterWidth;
  final double? masterViewWidth;
1481 1482 1483 1484 1485 1486 1487

  @override
  _MasterDetailScaffoldState createState() => _MasterDetailScaffoldState();
}

class _MasterDetailScaffoldState extends State<_MasterDetailScaffold>
    implements _PageOpener {
1488 1489 1490 1491
  late FloatingActionButtonLocation floatingActionButtonLocation;
  late double detailPageFABGutterWidth;
  late double detailPageFABlessGutterWidth;
  late double masterViewWidth;
1492

1493
  final ValueNotifier<Object?> _detailArguments = ValueNotifier<Object?>(null);
1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505

  @override
  void initState() {
    super.initState();
    detailPageFABlessGutterWidth = widget.detailPageFABlessGutterWidth ?? _kDetailPageFABlessGutterWidth;
    detailPageFABGutterWidth = widget.detailPageFABGutterWidth ?? _kDetailPageFABGutterWidth;
    masterViewWidth = widget.masterViewWidth ?? _kMasterViewWidth;
    floatingActionButtonLocation = widget.floatingActionButtonLocation ?? FloatingActionButtonLocation.endTop;
  }

  @override
  void openDetailPage(Object arguments) {
1506
    SchedulerBinding.instance!
1507
        .addPostFrameCallback((_) => _detailArguments.value = arguments);
1508
    _MasterDetailFlow.of(context)!.openDetailPage(arguments);
1509 1510 1511 1512
  }

  @override
  void setInitialDetailPage(Object arguments) {
1513
    SchedulerBinding.instance!
1514
        .addPostFrameCallback((_) => _detailArguments.value = arguments);
1515
    _MasterDetailFlow.of(context)!.setInitialDetailPage(arguments);
1516 1517 1518 1519 1520 1521 1522 1523 1524 1525
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Scaffold(
          floatingActionButtonLocation: floatingActionButtonLocation,
          appBar: AppBar(
            title: widget.title,
1526
            actions: widget.actionBuilder!(context, _ActionLevel.top),
1527 1528 1529 1530 1531 1532 1533 1534 1535
            leading: widget.leading,
            automaticallyImplyLeading: widget.automaticallyImplyLeading,
            centerTitle: widget.centerTitle,
            bottom: PreferredSize(
              preferredSize: const Size.fromHeight(kToolbarHeight),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.start,
                children: <Widget>[
                  ConstrainedBox(
1536
                    constraints: BoxConstraints.tightFor(width: masterViewWidth),
1537
                    child: IconTheme(
1538
                      data: Theme.of(context).primaryIconTheme,
1539 1540 1541 1542 1543 1544 1545 1546
                      child: Container(
                        alignment: AlignmentDirectional.centerEnd,
                        padding: const EdgeInsets.all(8),
                        child: OverflowBar(
                          spacing: 8,
                          overflowAlignment: OverflowBarAlignment.end,
                          children: widget.actionBuilder!(context, _ActionLevel.view),
                        ),
1547 1548
                      ),
                    ),
1549
                  ),
1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565
                ],
              ),
            ),
          ),
          body: _masterPanel(context),
          floatingActionButton: widget.floatingActionButton,
        ),
        // Detail view stacked above main scaffold and master view.
        SafeArea(
          child: Padding(
            padding: EdgeInsetsDirectional.only(
              start: masterViewWidth - _kCardElevation,
              end: widget.floatingActionButton == null
                  ? detailPageFABlessGutterWidth
                  : detailPageFABGutterWidth,
            ),
1566
            child: ValueListenableBuilder<Object?>(
1567
              valueListenable: _detailArguments,
1568
              builder: (BuildContext context, Object? value, Widget? child) {
1569
                return AnimatedSwitcher(
1570 1571 1572 1573 1574 1575 1576 1577
                  transitionBuilder: (Widget child, Animation<double> animation) =>
                    const FadeUpwardsPageTransitionsBuilder().buildTransitions<void>(
                      null,
                      null,
                      animation,
                      null,
                      child,
                    ),
1578 1579
                  duration: const Duration(milliseconds: 500),
                  child: Container(
1580
                    key: ValueKey<Object?>(value ?? widget.initialArguments),
1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602
                    constraints: const BoxConstraints.expand(),
                    child: _DetailView(
                      builder: widget.detailPageBuilder,
                      arguments: value ?? widget.initialArguments,
                    ),
                  ),
                );
              },
            ),
          ),
        ),
      ],
    );
  }

  ConstrainedBox _masterPanel(BuildContext context, {bool needsScaffold = false}) {
    return ConstrainedBox(
      constraints: BoxConstraints(maxWidth: masterViewWidth),
      child: needsScaffold
          ? Scaffold(
              appBar: AppBar(
                title: widget.title,
1603
                actions: widget.actionBuilder!(context, _ActionLevel.top),
1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616
                leading: widget.leading,
                automaticallyImplyLeading: widget.automaticallyImplyLeading,
                centerTitle: widget.centerTitle,
              ),
              body: widget.masterViewBuilder(context, true),
            )
          : widget.masterViewBuilder(context, true),
    );
  }
}

class _DetailView extends StatelessWidget {
  const _DetailView({
1617 1618 1619
    Key? key,
    required _DetailPageBuilder builder,
    Object? arguments,
1620 1621 1622 1623 1624 1625
  })  : assert(builder != null),
        _builder = builder,
        _arguments = arguments,
        super(key: key);

  final _DetailPageBuilder _builder;
1626
  final Object? _arguments;
1627 1628 1629 1630 1631 1632

  @override
  Widget build(BuildContext context) {
    if (_arguments == null) {
      return Container();
    }
1633
    final double screenHeight = MediaQuery.of(context).size.height;
1634 1635
    final double minHeight = (screenHeight - kToolbarHeight) / screenHeight;

1636 1637 1638 1639 1640 1641 1642 1643 1644
    return DraggableScrollableSheet(
      initialChildSize: minHeight,
      minChildSize: minHeight,
      maxChildSize: 1,
      expand: false,
      builder: (BuildContext context, ScrollController controller) {
        return MouseRegion(
          // TODO(TonicArtos): Remove MouseRegion workaround for pointer hover events passing through DraggableScrollableSheet once https://github.com/flutter/flutter/issues/59741 is resolved.
          child: Card(
1645
            color: Theme.of(context).cardColor,
1646 1647
            elevation: _kCardElevation,
            clipBehavior: Clip.antiAlias,
1648
            margin: const EdgeInsets.fromLTRB(_kCardElevation, 0.0, _kCardElevation, 0.0),
1649
            shape: const RoundedRectangleBorder(
1650
              borderRadius: BorderRadius.vertical(top: Radius.circular(3.0), bottom: Radius.zero),
1651
            ),
1652 1653 1654 1655 1656 1657 1658 1659
            child: _builder(
              context,
              _arguments,
              controller,
            ),
          ),
        );
      },
1660 1661 1662
    );
  }
}