about.dart 49.4 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 Flow, Timeline;
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
import 'floating_action_button_location.dart';
import 'ink_decoration.dart';
21
import 'list_tile.dart';
22
import 'material.dart';
23
import 'material_localizations.dart';
Ian Hickson's avatar
Ian Hickson committed
24
import 'page.dart';
25
import 'page_transitions_theme.dart';
Ian Hickson's avatar
Ian Hickson committed
26
import 'progress_indicator.dart';
Ian Hickson's avatar
Ian Hickson committed
27
import 'scaffold.dart';
28
import 'scrollbar.dart';
29
import 'text_button.dart';
30
import 'text_theme.dart';
Ian Hickson's avatar
Ian Hickson committed
31 32
import 'theme.dart';

33 34 35
// Examples can assume:
// BuildContext context;

36
/// A [ListTile] that shows an about box.
Ian Hickson's avatar
Ian Hickson committed
37
///
38 39
/// 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
40 41
///
/// The about box will include a button that shows licenses for software used by
42 43
/// 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
44 45 46
///
/// If your application does not have a [Drawer], you should provide an
/// affordance to call [showAboutDialog] or (at least) [showLicensePage].
47
///
48
/// {@tool dartpad}
49 50 51
/// This sample shows two ways to open [AboutDialog]. The first one
/// uses an [AboutListTile], and the second uses the [showAboutDialog] function.
///
52
/// ** See code in examples/api/lib/material/about/about_list_tile.0.dart **
53
/// {@end-tool}
54 55
class AboutListTile extends StatelessWidget {
  /// Creates a list tile for showing an about box.
Ian Hickson's avatar
Ian Hickson committed
56 57 58 59
  ///
  /// 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.
60
  const AboutListTile({
61
    super.key,
62
    this.icon,
Ian Hickson's avatar
Ian Hickson committed
63 64 65 66 67
    this.child,
    this.applicationName,
    this.applicationVersion,
    this.applicationIcon,
    this.applicationLegalese,
68
    this.aboutBoxChildren,
69
    this.dense,
70
  });
Ian Hickson's avatar
Ian Hickson committed
71 72 73 74 75 76 77

  /// 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.
78
  final Widget? icon;
Ian Hickson's avatar
Ian Hickson committed
79 80 81 82 83

  /// 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].
84
  final Widget? child;
Ian Hickson's avatar
Ian Hickson committed
85 86 87 88 89 90 91

  /// 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.
92
  /// Otherwise, defaults to [Platform.resolvedExecutable].
93
  final String? applicationName;
Ian Hickson's avatar
Ian Hickson committed
94 95 96 97 98 99

  /// The version of this build of the application.
  ///
  /// This string is shown under the application name in the [AboutDialog].
  ///
  /// Defaults to the empty string.
100
  final String? applicationVersion;
Ian Hickson's avatar
Ian Hickson committed
101 102 103 104 105

  /// The icon to show next to the application name in the [AboutDialog].
  ///
  /// By default no icon is shown.
  ///
106 107 108
  /// Typically this will be an [ImageIcon] widget. It should honor the
  /// [IconTheme]'s [IconThemeData.size].
  ///
Ian Hickson's avatar
Ian Hickson committed
109 110
  /// This is not necessarily the same as the icon shown on the drawer item
  /// itself, which is controlled by the [icon] property.
111
  final Widget? applicationIcon;
Ian Hickson's avatar
Ian Hickson committed
112 113 114 115 116 117

  /// A string to show in small print in the [AboutDialog].
  ///
  /// Typically this is a copyright notice.
  ///
  /// Defaults to the empty string.
118
  final String? applicationLegalese;
Ian Hickson's avatar
Ian Hickson committed
119 120 121 122 123 124 125

  /// 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.
126
  final List<Widget>? aboutBoxChildren;
Ian Hickson's avatar
Ian Hickson committed
127

128 129
  /// Whether this list tile is part of a vertically dense list.
  ///
130
  /// If this property is null, then its value is based on [ListTileThemeData.dense].
131 132
  ///
  /// Dense list tiles default to a smaller height.
133
  final bool? dense;
134

Ian Hickson's avatar
Ian Hickson committed
135 136 137
  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterial(context));
138
    assert(debugCheckHasMaterialLocalizations(context));
139
    return ListTile(
140
      leading: icon,
141
      title: child ?? Text(MaterialLocalizations.of(context).aboutListTileTitle(
142 143 144
        applicationName ?? _defaultApplicationName(context),
      )),
      dense: dense,
145
      onTap: () {
Ian Hickson's avatar
Ian Hickson committed
146 147 148 149 150 151
        showAboutDialog(
          context: context,
          applicationName: applicationName,
          applicationVersion: applicationVersion,
          applicationIcon: applicationIcon,
          applicationLegalese: applicationLegalese,
152
          children: aboutBoxChildren,
Ian Hickson's avatar
Ian Hickson committed
153
        );
154
      },
Ian Hickson's avatar
Ian Hickson committed
155 156 157 158 159 160 161 162 163
    );
  }
}

/// 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].
///
164
/// If the application has a [Drawer], consider using [AboutListTile] instead
Ian Hickson's avatar
Ian Hickson committed
165 166 167 168
/// 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].
169 170 171
///
/// 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
172
///
173 174 175
/// The [context], [useRootNavigator], [routeSettings] and [anchorPoint]
/// arguments are passed to [showDialog], the documentation for which discusses
/// how it is used.
Ian Hickson's avatar
Ian Hickson committed
176
void showAboutDialog({
177 178 179 180 181 182
  required BuildContext context,
  String? applicationName,
  String? applicationVersion,
  Widget? applicationIcon,
  String? applicationLegalese,
  List<Widget>? children,
183
  bool useRootNavigator = true,
184
  RouteSettings? routeSettings,
185
  Offset? anchorPoint,
Ian Hickson's avatar
Ian Hickson committed
186
}) {
187
  showDialog<void>(
Ian Hickson's avatar
Ian Hickson committed
188
    context: context,
189
    useRootNavigator: useRootNavigator,
190
    builder: (BuildContext context) {
191
      return AboutDialog(
192 193 194 195 196 197
        applicationName: applicationName,
        applicationVersion: applicationVersion,
        applicationIcon: applicationIcon,
        applicationLegalese: applicationLegalese,
        children: children,
      );
198
    },
199
    routeSettings: routeSettings,
200
    anchorPoint: anchorPoint,
Ian Hickson's avatar
Ian Hickson committed
201 202 203 204 205 206
  );
}

/// Displays a [LicensePage], which shows licenses for software used by the
/// application.
///
207 208 209 210 211 212 213
/// 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
214
///
215
/// If the application has a [Drawer], consider using [AboutListTile] instead
Ian Hickson's avatar
Ian Hickson committed
216 217 218 219
/// of calling this directly.
///
/// The [AboutDialog] shown by [showAboutDialog] includes a button that calls
/// [showLicensePage].
220 221 222
///
/// 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
223
void showLicensePage({
224 225 226 227 228
  required BuildContext context,
  String? applicationName,
  String? applicationVersion,
  Widget? applicationIcon,
  String? applicationLegalese,
229
  bool useRootNavigator = false,
Ian Hickson's avatar
Ian Hickson committed
230
}) {
231
  Navigator.of(context, rootNavigator: useRootNavigator).push(MaterialPageRoute<void>(
232
    builder: (BuildContext context) => LicensePage(
233 234
      applicationName: applicationName,
      applicationVersion: applicationVersion,
235
      applicationIcon: applicationIcon,
236
      applicationLegalese: applicationLegalese,
237
    ),
238
  ));
Ian Hickson's avatar
Ian Hickson committed
239 240
}

241 242 243
/// The amount of vertical space to separate chunks of text.
const double _textVerticalSeparation = 18.0;

Ian Hickson's avatar
Ian Hickson committed
244 245 246 247 248
/// 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].
249
///
250 251
/// {@youtube 560 315 https://www.youtube.com/watch?v=YFCSODyFxbE}
///
252
/// If the application has a [Drawer], the [AboutListTile] widget can make the
253 254 255 256 257 258 259
/// 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
260 261 262 263 264 265
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.
266
  const AboutDialog({
267
    super.key,
Ian Hickson's avatar
Ian Hickson committed
268 269 270 271
    this.applicationName,
    this.applicationVersion,
    this.applicationIcon,
    this.applicationLegalese,
272
    this.children,
273
  });
Ian Hickson's avatar
Ian Hickson committed
274 275 276 277

  /// The name of the application.
  ///
  /// Defaults to the value of [Title.title], if a [Title] widget can be found.
278
  /// Otherwise, defaults to [Platform.resolvedExecutable].
279
  final String? applicationName;
Ian Hickson's avatar
Ian Hickson committed
280 281 282 283 284 285

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

  /// The icon to show next to the application name.
  ///
  /// By default no icon is shown.
291 292 293
  ///
  /// Typically this will be an [ImageIcon] widget. It should honor the
  /// [IconTheme]'s [IconThemeData.size].
294
  final Widget? applicationIcon;
Ian Hickson's avatar
Ian Hickson committed
295 296 297 298 299 300

  /// A string to show in small print.
  ///
  /// Typically this is a copyright notice.
  ///
  /// Defaults to the empty string.
301
  final String? applicationLegalese;
Ian Hickson's avatar
Ian Hickson committed
302 303 304 305 306 307 308

  /// 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.
309
  final List<Widget>? children;
Ian Hickson's avatar
Ian Hickson committed
310 311 312

  @override
  Widget build(BuildContext context) {
313
    assert(debugCheckHasMaterialLocalizations(context));
Ian Hickson's avatar
Ian Hickson committed
314 315
    final String name = applicationName ?? _defaultApplicationName(context);
    final String version = applicationVersion ?? _defaultApplicationVersion(context);
316
    final Widget? icon = applicationIcon ?? _defaultApplicationIcon(context);
317 318
    final ThemeData themeData = Theme.of(context);
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
319
    return AlertDialog(
320 321 322 323 324
      content: ListBody(
        children: <Widget>[
          Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
325
              if (icon != null) IconTheme(data: themeData.iconTheme, child: icon),
326 327 328 329 330
              Expanded(
                child: Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 24.0),
                  child: ListBody(
                    children: <Widget>[
331 332
                      Text(name, style: themeData.textTheme.headlineSmall),
                      Text(version, style: themeData.textTheme.bodyMedium),
333
                      const SizedBox(height: _textVerticalSeparation),
334
                      Text(applicationLegalese ?? '', style: themeData.textTheme.bodySmall),
335
                    ],
336 337
                  ),
                ),
338 339 340 341 342
              ),
            ],
          ),
          ...?children,
        ],
343
      ),
Ian Hickson's avatar
Ian Hickson committed
344
      actions: <Widget>[
345
        TextButton(
346 347 348 349 350
          child: Text(
            themeData.useMaterial3
              ? localizations.viewLicensesButtonLabel
              : localizations.viewLicensesButtonLabel.toUpperCase()
          ),
Ian Hickson's avatar
Ian Hickson committed
351 352 353 354 355 356
          onPressed: () {
            showLicensePage(
              context: context,
              applicationName: applicationName,
              applicationVersion: applicationVersion,
              applicationIcon: applicationIcon,
357
              applicationLegalese: applicationLegalese,
Ian Hickson's avatar
Ian Hickson committed
358
            );
359
          },
Ian Hickson's avatar
Ian Hickson committed
360
        ),
361
        TextButton(
362 363 364 365 366
          child: Text(
            themeData.useMaterial3
              ? localizations.closeButtonLabel
              : localizations.closeButtonLabel.toUpperCase()
          ),
Ian Hickson's avatar
Ian Hickson committed
367 368
          onPressed: () {
            Navigator.pop(context);
369
          },
Ian Hickson's avatar
Ian Hickson committed
370
        ),
371
      ],
372
      scrollable: true,
Ian Hickson's avatar
Ian Hickson committed
373 374 375 376 377 378 379
    );
  }
}

/// A page that shows licenses for software used by the application.
///
/// To show a [LicensePage], use [showLicensePage].
380
///
381
/// The [AboutDialog] shown by [showAboutDialog] and [AboutListTile] includes
382 383 384 385
/// 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.
386
class LicensePage extends StatefulWidget {
Ian Hickson's avatar
Ian Hickson committed
387 388 389 390 391
  /// 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.
392 393 394
  ///
  /// 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
395
  const LicensePage({
396
    super.key,
Ian Hickson's avatar
Ian Hickson committed
397 398
    this.applicationName,
    this.applicationVersion,
399
    this.applicationIcon,
400
    this.applicationLegalese,
401
  });
Ian Hickson's avatar
Ian Hickson committed
402 403 404 405

  /// The name of the application.
  ///
  /// Defaults to the value of [Title.title], if a [Title] widget can be found.
406
  /// Otherwise, defaults to [Platform.resolvedExecutable].
407
  final String? applicationName;
Ian Hickson's avatar
Ian Hickson committed
408 409 410 411 412 413

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

416 417 418 419 420 421
  /// 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].
422
  final Widget? applicationIcon;
423

Ian Hickson's avatar
Ian Hickson committed
424 425 426 427 428
  /// A string to show in small print.
  ///
  /// Typically this is a copyright notice.
  ///
  /// Defaults to the empty string.
429
  final String? applicationLegalese;
Ian Hickson's avatar
Ian Hickson committed
430

431
  @override
432
  State<LicensePage> createState() => _LicensePageState();
433 434 435
}

class _LicensePageState extends State<LicensePage> {
436
  final ValueNotifier<int?> selectedId = ValueNotifier<int?>(null);
437

438 439 440 441 442 443
  @override
  void dispose() {
    selectedId.dispose();
    super.dispose();
  }

444 445 446 447
  @override
  Widget build(BuildContext context) {
    return _MasterDetailFlow(
      detailPageFABlessGutterWidth: _getGutterSize(context),
448
      title: Text(MaterialLocalizations.of(context).licensesPageTitle),
449 450 451 452 453
      detailPageBuilder: _packageLicensePage,
      masterViewBuilder: _packagesView,
    );
  }

454
  Widget _packageLicensePage(BuildContext _, Object? args, ScrollController? scrollController) {
455
    assert(args is _DetailArguments);
456
    final _DetailArguments detailArguments = args! as _DetailArguments;
457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480
    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({
481 482
    required this.name,
    required this.version,
483 484
    this.icon,
    this.legalese,
485
  });
486 487 488

  final String name;
  final String version;
489 490
  final Widget? icon;
  final String? legalese;
491 492 493 494 495 496 497 498 499 500 501 502

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.symmetric(
        horizontal: _getGutterSize(context),
        vertical: 24.0,
      ),
      child: Column(
        children: <Widget>[
          Text(
            name,
503
            style: Theme.of(context).textTheme.headlineSmall,
504 505 506
            textAlign: TextAlign.center,
          ),
          if (icon != null)
507
            IconTheme(data: Theme.of(context).iconTheme, child: icon!),
508 509 510 511 512
          if (version != '')
            Padding(
              padding: const EdgeInsets.only(bottom: _textVerticalSeparation),
              child: Text(
                version,
513
                style: Theme.of(context).textTheme.bodyMedium,
514 515 516 517 518 519
                textAlign: TextAlign.center,
              ),
            ),
          if (legalese != null && legalese != '')
            Text(
              legalese!,
520
              style: Theme.of(context).textTheme.bodySmall,
521 522
              textAlign: TextAlign.center,
            ),
523 524 525
          const SizedBox(height: _textVerticalSeparation),
          Text(
            'Powered by Flutter',
526
            style: Theme.of(context).textTheme.bodyMedium,
527 528 529 530 531 532 533 534 535 536
            textAlign: TextAlign.center,
          ),
        ],
      ),
    );
  }
}

class _PackagesView extends StatefulWidget {
  const _PackagesView({
537 538 539
    required this.about,
    required this.isLateral,
    required this.selectedId,
540
  });
541 542 543

  final Widget about;
  final bool isLateral;
544
  final ValueNotifier<int?> selectedId;
545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562

  @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) {
563 564 565 566 567
        return LayoutBuilder(
          key: ValueKey<ConnectionState>(snapshot.connectionState),
          builder: (BuildContext context, BoxConstraints constraints) {
            switch (snapshot.connectionState) {
              case ConnectionState.done:
568 569 570 571 572 573 574 575 576 577 578
                if (snapshot.hasError) {
                  assert(() {
                    FlutterError.reportError(FlutterErrorDetails(
                      exception: snapshot.error!,
                      stack: snapshot.stackTrace,
                      context: ErrorDescription('while decoding the license file'),
                    ));
                    return true;
                  }());
                  return Center(child: Text(snapshot.error.toString()));
                }
579 580
                _initDefaultDetailPage(snapshot.data!, context);
                return ValueListenableBuilder<int?>(
581
                  valueListenable: widget.selectedId,
582
                  builder: (BuildContext context, int? selectedId, Widget? _) {
583 584
                    return Center(
                      child: Material(
585
                        color: Theme.of(context).cardColor,
586 587 588
                        elevation: 4.0,
                        child: Container(
                          constraints: BoxConstraints.loose(const Size.fromWidth(600.0)),
589
                          child: _packagesList(context, selectedId, snapshot.data!, widget.isLateral),
590
                        ),
591 592 593 594
                      ),
                    );
                  },
                );
595 596 597
              case ConnectionState.none:
              case ConnectionState.active:
              case ConnectionState.waiting:
598
                return Material(
599
                    color: Theme.of(context).cardColor,
600
                    child: Column(
601 602 603 604
                    children: <Widget>[
                      widget.about,
                      const Center(child: CircularProgressIndicator()),
                    ],
605 606 607 608
                  ),
                );
            }
          },
609 610 611 612 613 614
        );
      },
    );
  }

  void _initDefaultDetailPage(_LicenseData data, BuildContext context) {
615 616 617
    if (data.packages.isEmpty) {
      return;
    }
618
    final String packageName = data.packages[widget.selectedId.value ?? 0];
619
    final List<int> bindings = data.packageLicenseBindings[packageName]!;
620
    _MasterDetailFlow.of(context).setInitialDetailPage(
621 622 623 624 625 626 627 628 629
      _DetailArguments(
        packageName,
        bindings.map((int i) => data.licenses[i]).toList(growable: false),
      ),
    );
  }

  Widget _packagesList(
    final BuildContext context,
630
    final int? selectedId,
631 632 633
    final _LicenseData data,
    final bool drawSelection,
  ) {
634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649
    return ListView.builder(
      itemCount: data.packages.length + 1,
      itemBuilder: (BuildContext context, int index) {
        if (index == 0) {
          return widget.about;
        }
        final int packageIndex = index - 1;
        final String packageName = data.packages[packageIndex];
        final List<int> bindings = data.packageLicenseBindings[packageName]!;
        return _PackageListTile(
          packageName: packageName,
          index: packageIndex,
          isSelected: drawSelection && packageIndex == (selectedId ?? 0),
          numberLicenses: bindings.length,
          onTap: () {
            widget.selectedId.value = packageIndex;
650
            _MasterDetailFlow.of(context).openDetailPage(_DetailArguments(
651 652 653 654 655 656
              packageName,
              bindings.map((int i) => data.licenses[i]).toList(growable: false),
            ));
          },
        );
      },
657 658 659 660 661 662
    );
  }
}

class _PackageListTile extends StatelessWidget {
  const _PackageListTile({
663
    required this.packageName,
664
    this.index,
665 666
    required this.isSelected,
    required this.numberLicenses,
667
    this.onTap,
668
});
669 670

  final String packageName;
671
  final int? index;
672 673
  final bool isSelected;
  final int numberLicenses;
674
  final GestureTapCallback? onTap;
675 676 677 678

  @override
  Widget build(BuildContext context) {
    return Ink(
679
      color: isSelected ? Theme.of(context).highlightColor : Theme.of(context).cardColor,
680 681
      child: ListTile(
        title: Text(packageName),
682
        subtitle: Text(MaterialLocalizations.of(context).licensesPackageDetailText(numberLicenses)),
683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699
        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.
700
  String? firstPackage;
701 702 703 704 705 706 707 708 709

  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.
710
      packageLicenseBindings[package]!.add(licenses.length);
711 712 713 714
    }
    licenses.add(entry); // Completion of the contract above.
  }

715
  /// Add a package and initialize package license binding. This is a no-op if
716 717 718 719 720 721 722 723 724 725 726 727
  /// 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.
728
  void sortPackages([int Function(String a, String b)? compare]) {
729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752
    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
753
  bool operator ==(final Object other) {
754 755 756 757 758 759 760
    if (other is _DetailArguments) {
      return other.packageName == packageName;
    }
    return other == this;
  }

  @override
761
  int get hashCode => Object.hash(packageName, Object.hashAll(licenseEntries));
762 763 764 765
}

class _PackageLicensePage extends StatefulWidget {
  const _PackageLicensePage({
766 767 768
    required this.packageName,
    required this.licenseEntries,
    required this.scrollController,
769
  });
770 771 772

  final String packageName;
  final List<LicenseEntry> licenseEntries;
773
  final ScrollController? scrollController;
774 775 776 777 778 779

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

class _PackageLicensePageState extends State<_PackageLicensePage> {
Ian Hickson's avatar
Ian Hickson committed
780 781 782 783 784 785
  @override
  void initState() {
    super.initState();
    _initLicenses();
  }

786
  final List<Widget> _licenses = <Widget>[];
Ian Hickson's avatar
Ian Hickson committed
787 788
  bool _loaded = false;

789
  Future<void> _initLicenses() async {
790 791 792 793 794 795 796
    int debugFlowId = -1;
    assert(() {
      final Flow flow = Flow.begin();
      Timeline.timeSync('_initLicenses()', () { }, flow: flow);
      debugFlowId = flow.id;
      return true;
    }());
797
    for (final LicenseEntry license in widget.licenseEntries) {
798
      if (!mounted) {
799
        return;
800 801 802 803 804
      }
      assert(() {
        Timeline.timeSync('_initLicenses()', () { }, flow: Flow.step(debugFlowId));
        return true;
      }());
805
      final List<LicenseParagraph> paragraphs =
806
        await SchedulerBinding.instance.scheduleTask<List<LicenseParagraph>>(
807
          license.paragraphs.toList,
808 809 810
          Priority.animation,
          debugLabel: 'License',
        );
811 812 813
      if (!mounted) {
        return;
      }
Ian Hickson's avatar
Ian Hickson committed
814
      setState(() {
815
        _licenses.add(const Padding(
816 817
          padding: EdgeInsets.all(18.0),
          child: Divider(),
Ian Hickson's avatar
Ian Hickson committed
818
        ));
819
        for (final LicenseParagraph paragraph in paragraphs) {
Ian Hickson's avatar
Ian Hickson committed
820
          if (paragraph.indent == LicenseParagraph.centeredIndent) {
821
            _licenses.add(Padding(
822
              padding: const EdgeInsets.only(top: 16.0),
823
              child: Text(
Ian Hickson's avatar
Ian Hickson committed
824
                paragraph.text,
825
                style: const TextStyle(fontWeight: FontWeight.bold),
826 827
                textAlign: TextAlign.center,
              ),
Ian Hickson's avatar
Ian Hickson committed
828 829 830
            ));
          } else {
            assert(paragraph.indent >= 0);
831 832
            _licenses.add(Padding(
              padding: EdgeInsetsDirectional.only(top: 8.0, start: 16.0 * paragraph.indent),
833
              child: Text(paragraph.text),
Ian Hickson's avatar
Ian Hickson committed
834 835
            ));
          }
836
        }
Ian Hickson's avatar
Ian Hickson committed
837
      });
838
    }
Ian Hickson's avatar
Ian Hickson committed
839 840 841
    setState(() {
      _loaded = true;
    });
842 843 844 845
    assert(() {
      Timeline.timeSync('Build scheduled', () { }, flow: Flow.end(debugFlowId));
      return true;
    }());
846 847
  }

Ian Hickson's avatar
Ian Hickson committed
848 849
  @override
  Widget build(BuildContext context) {
850
    assert(debugCheckHasMaterialLocalizations(context));
851
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
852
    final ThemeData theme = Theme.of(context);
853 854 855 856 857 858 859 860 861 862 863 864 865 866 867
    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(),
          ),
        ),
    ];

868
    final Widget page;
869 870 871
    if (widget.scrollController == null) {
      page = Scaffold(
        appBar: AppBar(
872 873 874
          title: _PackageLicensePageTitle(
            title,
            subtitle,
875 876
            theme.primaryTextTheme,
            theme.appBarTheme.titleTextStyle,
877
          ),
878 879 880 881 882 883 884 885 886 887
        ),
        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,
888 889 890 891 892 893
                child: ScrollConfiguration(
                  // A Scrollbar is built-in below.
                  behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
                  child: Scrollbar(
                    child: ListView(padding: padding, children: listWidgets),
                  ),
894
                ),
895
              ),
Hans Muller's avatar
Hans Muller committed
896
            ),
897 898
          ),
        ),
899 900 901 902 903 904 905 906
      );
    } else {
      page = CustomScrollView(
        controller: widget.scrollController,
        slivers: <Widget>[
          SliverAppBar(
            automaticallyImplyLeading: false,
            pinned: true,
907
            backgroundColor: theme.cardColor,
908
            title: _PackageLicensePageTitle(title, subtitle, theme.textTheme, theme.textTheme.titleLarge),
909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926
          ),
          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(
927
      style: theme.textTheme.bodySmall!,
928 929 930 931 932 933 934 935 936
      child: page,
    );
  }
}

class _PackageLicensePageTitle extends StatelessWidget {
  const _PackageLicensePageTitle(
    this.title,
    this.subtitle,
937
    this.theme,
938
    this.titleTextStyle,
939
  );
940 941 942 943

  final String title;
  final String subtitle;
  final TextTheme theme;
944
  final TextStyle? titleTextStyle;
945 946 947

  @override
  Widget build(BuildContext context) {
948
    final Color? color = Theme.of(context).appBarTheme.foregroundColor;
949
    final TextStyle? effectiveTitleTextStyle = titleTextStyle ?? theme.titleLarge;
950

951 952 953 954
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
955
        Text(title, style: effectiveTitleTextStyle?.copyWith(color: color)),
956
        Text(subtitle, style: theme.titleSmall?.copyWith(color: color)),
957
      ],
Ian Hickson's avatar
Ian Hickson committed
958 959 960 961 962
    );
  }
}

String _defaultApplicationName(BuildContext context) {
963 964 965 966 967 968
  // 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.
969
  final Title? ancestorTitle = context.findAncestorWidgetOfExactType<Title>();
970
  return ancestorTitle?.title ?? Platform.resolvedExecutable.split(Platform.pathSeparator).last;
Ian Hickson's avatar
Ian Hickson committed
971 972 973 974 975 976 977
}

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

978
Widget? _defaultApplicationIcon(BuildContext context) {
Ian Hickson's avatar
Ian Hickson committed
979 980 981
  // TODO(ianh): Get this from the embedder somehow.
  return null;
}
982 983 984 985 986 987

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

double _getGutterSize(BuildContext context) =>
988
    MediaQuery.sizeOf(context).width >= _materialGutterThreshold ? _wideGutterSize : _narrowGutterSize;
989 990 991 992 993 994 995 996

/// 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.
997
typedef _DetailPageBuilder = Widget Function(BuildContext context, Object? arguments, ScrollController? scrollController);
998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042

/// 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,
}

/// 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({
1043 1044
    required this.detailPageBuilder,
    required this.masterViewBuilder,
1045
    this.automaticallyImplyLeading = true, // ignore: unused_element
1046
    this.detailPageFABlessGutterWidth,
1047
    this.displayMode = _LayoutMode.auto, // ignore: unused_element
1048
    this.title,
1049
  });
1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065

  /// 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 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 gutter when there is no floating action button.
1066
  final double? detailPageFABlessGutterWidth;
1067 1068 1069 1070

  /// The title for the lateral UI [AppBar].
  ///
  /// See [AppBar.title].
1071
  final Widget? title;
1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083

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

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

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

1084 1085 1086 1087 1088 1089 1090 1091
  // 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);
  // ```
1092
  static _MasterDetailFlowProxy of(BuildContext context) {
1093
    _PageOpener? pageOpener = context.findAncestorStateOfType<_MasterDetailScaffoldState>();
1094 1095
    pageOpener ??= context.findAncestorStateOfType<_MasterDetailFlowState>();
    assert(() {
1096
      if (pageOpener == null) {
1097
        throw FlutterError(
1098 1099
          '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 '
1100
          'that of a widget that is a descendant of a Master Detail Flow widget.',
1101
        );
1102 1103 1104
      }
      return true;
    }());
1105
    return _MasterDetailFlowProxy._(pageOpener!);
1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135
  }
}

/// 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 {
1136
  /// Tracks whether focus is on the detail or master views. Determines behavior when switching
1137 1138 1139 1140
  /// from lateral to nested navigation.
  _Focus focus = _Focus.master;

  /// Cache of arguments passed when opening a detail page. Used when rebuilding.
1141
  Object? _cachedDetailArguments;
1142 1143

  /// Record of the layout that was built.
1144
  _LayoutMode? _builtLayout;
1145 1146 1147 1148 1149 1150 1151 1152

  /// 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) {
1153
      _navigatorKey.currentState!.pushNamed(_navDetail, arguments: arguments);
1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171
    } 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:
1172 1173
        return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
          final double availableWidth = constraints.maxWidth;
1174
          if (availableWidth >= _materialWideDisplayThreshold) {
1175 1176 1177 1178 1179
            return _lateralUI(context);
          } else {
            return _nestedUI(context);
          }
        });
1180 1181 1182 1183 1184 1185 1186 1187 1188
    }
  }

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

    return WillPopScope(
      // Push pop check into nested navigator.
1189
      onWillPop: () async => !(await _navigatorKey.currentState!.maybePop()),
1190 1191 1192 1193 1194 1195 1196 1197 1198 1199
      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,
1200
                _detailPageRoute(_cachedDetailArguments),
1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226
              ];
          }
        },
        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(
1227 1228
        child: _MasterPage(
                leading: widget.automaticallyImplyLeading && Navigator.of(context).canPop()
1229
                        ? BackButton(onPressed: () => Navigator.of(context).pop())
1230
                        : null,
1231 1232 1233 1234 1235 1236 1237 1238
                title: widget.title,
                automaticallyImplyLeading: widget.automaticallyImplyLeading,
                masterViewBuilder: widget.masterViewBuilder,
              ),
      ),
    );
  }

1239
  MaterialPageRoute<void> _detailPageRoute(Object? arguments) {
1240 1241 1242 1243 1244
    return MaterialPageRoute<dynamic>(builder: (BuildContext context) {
      return WillPopScope(
        onWillPop: () async {
          // No need for setState() as rebuild happens on navigation pop.
          focus = _Focus.master;
1245
          Navigator.of(context).pop();
1246 1247 1248 1249 1250 1251 1252 1253 1254 1255
          return false;
        },
        child: BlockSemantics(child: widget.detailPageBuilder(context, arguments, null)),
      );
    });
  }

  Widget _lateralUI(BuildContext context) {
    _builtLayout = _LayoutMode.lateral;
    return _MasterDetailScaffold(
1256
      actionBuilder: (_, __) => const<Widget>[],
1257
      automaticallyImplyLeading: widget.automaticallyImplyLeading,
1258
      detailPageBuilder: (BuildContext context, Object? args, ScrollController? scrollController) =>
1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272
          widget.detailPageBuilder(context, args ?? _cachedDetailArguments, scrollController),
      detailPageFABlessGutterWidth: widget.detailPageFABlessGutterWidth,
      initialArguments: _cachedDetailArguments,
      masterViewBuilder: (BuildContext context, bool isLateral) => widget.masterViewBuilder(context, isLateral),
      title: widget.title,
    );
  }
}

class _MasterPage extends StatelessWidget {
  const _MasterPage({
    this.leading,
    this.title,
    this.masterViewBuilder,
1273
    required this.automaticallyImplyLeading,
1274
  });
1275

1276 1277 1278
  final _MasterViewBuilder? masterViewBuilder;
  final Widget? title;
  final Widget? leading;
1279 1280 1281 1282
  final bool automaticallyImplyLeading;

  @override
  Widget build(BuildContext context) {
1283
    return Scaffold(
1284 1285 1286
        appBar: AppBar(
          title: title,
          leading: leading,
1287
          actions: const <Widget>[],
1288 1289
          automaticallyImplyLeading: automaticallyImplyLeading,
        ),
1290
        body: masterViewBuilder!(context, false),
1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302
      );
  }

}

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({
1303 1304
    required this.detailPageBuilder,
    required this.masterViewBuilder,
1305 1306 1307
    this.actionBuilder,
    this.initialArguments,
    this.title,
1308
    required this.automaticallyImplyLeading,
1309
    this.detailPageFABlessGutterWidth,
1310
  });
1311 1312 1313 1314 1315 1316 1317 1318 1319

  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;
1320 1321 1322
  final _ActionBuilder? actionBuilder;
  final Object? initialArguments;
  final Widget? title;
1323
  final bool automaticallyImplyLeading;
1324
  final double? detailPageFABlessGutterWidth;
1325 1326 1327 1328 1329 1330 1331

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

class _MasterDetailScaffoldState extends State<_MasterDetailScaffold>
    implements _PageOpener {
1332 1333 1334 1335
  late FloatingActionButtonLocation floatingActionButtonLocation;
  late double detailPageFABGutterWidth;
  late double detailPageFABlessGutterWidth;
  late double masterViewWidth;
1336

1337
  final ValueNotifier<Object?> _detailArguments = ValueNotifier<Object?>(null);
1338 1339 1340 1341 1342

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

1348 1349 1350 1351 1352 1353
  @override
  void dispose() {
    _detailArguments.dispose();
    super.dispose();
  }

1354 1355
  @override
  void openDetailPage(Object arguments) {
1356
    SchedulerBinding.instance.addPostFrameCallback((_) => _detailArguments.value = arguments);
1357
    _MasterDetailFlow.of(context).openDetailPage(arguments);
1358 1359 1360 1361
  }

  @override
  void setInitialDetailPage(Object arguments) {
1362
    SchedulerBinding.instance.addPostFrameCallback((_) => _detailArguments.value = arguments);
1363
    _MasterDetailFlow.of(context).setInitialDetailPage(arguments);
1364 1365 1366 1367 1368 1369 1370 1371 1372 1373
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Scaffold(
          floatingActionButtonLocation: floatingActionButtonLocation,
          appBar: AppBar(
            title: widget.title,
1374
            actions: widget.actionBuilder!(context, _ActionLevel.top),
1375 1376 1377 1378 1379 1380
            automaticallyImplyLeading: widget.automaticallyImplyLeading,
            bottom: PreferredSize(
              preferredSize: const Size.fromHeight(kToolbarHeight),
              child: Row(
                children: <Widget>[
                  ConstrainedBox(
1381
                    constraints: BoxConstraints.tightFor(width: masterViewWidth),
1382
                    child: IconTheme(
1383
                      data: Theme.of(context).primaryIconTheme,
1384 1385 1386 1387 1388 1389 1390 1391
                      child: Container(
                        alignment: AlignmentDirectional.centerEnd,
                        padding: const EdgeInsets.all(8),
                        child: OverflowBar(
                          spacing: 8,
                          overflowAlignment: OverflowBarAlignment.end,
                          children: widget.actionBuilder!(context, _ActionLevel.view),
                        ),
1392 1393
                      ),
                    ),
1394
                  ),
1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405
                ],
              ),
            ),
          ),
          body: _masterPanel(context),
        ),
        // Detail view stacked above main scaffold and master view.
        SafeArea(
          child: Padding(
            padding: EdgeInsetsDirectional.only(
              start: masterViewWidth - _kCardElevation,
1406
              end: detailPageFABlessGutterWidth,
1407
            ),
1408
            child: ValueListenableBuilder<Object?>(
1409
              valueListenable: _detailArguments,
1410
              builder: (BuildContext context, Object? value, Widget? child) {
1411
                return AnimatedSwitcher(
1412 1413 1414 1415 1416 1417 1418 1419
                  transitionBuilder: (Widget child, Animation<double> animation) =>
                    const FadeUpwardsPageTransitionsBuilder().buildTransitions<void>(
                      null,
                      null,
                      animation,
                      null,
                      child,
                    ),
1420 1421
                  duration: const Duration(milliseconds: 500),
                  child: Container(
1422
                    key: ValueKey<Object?>(value ?? widget.initialArguments),
1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444
                    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,
1445
                actions: widget.actionBuilder!(context, _ActionLevel.top),
1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456
                automaticallyImplyLeading: widget.automaticallyImplyLeading,
              ),
              body: widget.masterViewBuilder(context, true),
            )
          : widget.masterViewBuilder(context, true),
    );
  }
}

class _DetailView extends StatelessWidget {
  const _DetailView({
1457 1458
    required _DetailPageBuilder builder,
    Object? arguments,
1459
  })  : _builder = builder,
1460
        _arguments = arguments;
1461 1462

  final _DetailPageBuilder _builder;
1463
  final Object? _arguments;
1464 1465 1466 1467

  @override
  Widget build(BuildContext context) {
    if (_arguments == null) {
1468
      return const SizedBox.shrink();
1469
    }
1470
    final double screenHeight = MediaQuery.sizeOf(context).height;
1471 1472
    final double minHeight = (screenHeight - kToolbarHeight) / screenHeight;

1473 1474 1475 1476 1477 1478 1479 1480
    return DraggableScrollableSheet(
      initialChildSize: minHeight,
      minChildSize: minHeight,
      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(
1481
            color: Theme.of(context).cardColor,
1482 1483
            elevation: _kCardElevation,
            clipBehavior: Clip.antiAlias,
1484
            margin: const EdgeInsets.fromLTRB(_kCardElevation, 0.0, _kCardElevation, 0.0),
1485
            shape: const RoundedRectangleBorder(
1486
              borderRadius: BorderRadius.vertical(top: Radius.circular(3.0)),
1487
            ),
1488 1489 1490 1491 1492 1493 1494 1495
            child: _builder(
              context,
              _arguments,
              controller,
            ),
          ),
        );
      },
1496 1497 1498
    );
  }
}