| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:developer' show Timeline, Flow; |
| import 'dart:io' show Platform; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/scheduler.dart'; |
| import 'package:flutter/widgets.dart' hide Flow; |
| |
| import 'app_bar.dart'; |
| import 'debug.dart'; |
| import 'dialog.dart'; |
| import 'flat_button.dart'; |
| import 'list_tile.dart'; |
| import 'material_localizations.dart'; |
| import 'page.dart'; |
| import 'progress_indicator.dart'; |
| import 'scaffold.dart'; |
| import 'scrollbar.dart'; |
| import 'theme.dart'; |
| |
| /// A [ListTile] that shows an about box. |
| /// |
| /// This widget is often added to an app's [Drawer]. When tapped it shows |
| /// an about box dialog with [showAboutDialog]. |
| /// |
| /// The about box will include a button that shows licenses for software used by |
| /// the application. The licenses shown are those returned by the |
| /// [LicenseRegistry] API, which can be used to add more licenses to the list. |
| /// |
| /// If your application does not have a [Drawer], you should provide an |
| /// affordance to call [showAboutDialog] or (at least) [showLicensePage]. |
| /// {@tool dartpad --template=stateless_widget_material} |
| /// |
| /// 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) { |
| /// final TextStyle textStyle = Theme.of(context).textTheme.bodyText2; |
| /// final List<Widget> aboutBoxChildren = <Widget>[ |
| /// SizedBox(height: 24), |
| /// RichText( |
| /// text: TextSpan( |
| /// children: <TextSpan>[ |
| /// TextSpan( |
| /// style: textStyle, |
| /// text: 'Flutter is Google’s UI toolkit for building beautiful, ' |
| /// 'natively compiled applications for mobile, web, and desktop ' |
| /// 'from a single codebase. Learn more about Flutter at ' |
| /// ), |
| /// TextSpan( |
| /// style: textStyle.copyWith(color: Theme.of(context).accentColor), |
| /// text: 'https://flutter.dev' |
| /// ), |
| /// TextSpan( |
| /// style: textStyle, |
| /// text: '.' |
| /// ), |
| /// ], |
| /// ), |
| /// ), |
| /// ]; |
| /// |
| /// return Scaffold( |
| /// appBar: AppBar( |
| /// title: Text('Show About Example'), |
| /// ), |
| /// drawer: Drawer( |
| /// child: SingleChildScrollView( |
| /// child: SafeArea( |
| /// child: AboutListTile( |
| /// icon: Icon(Icons.info), |
| /// applicationIcon: FlutterLogo(), |
| /// applicationName: 'Show About Example', |
| /// applicationVersion: 'August 2019', |
| /// applicationLegalese: '© 2014 The Flutter Authors', |
| /// aboutBoxChildren: aboutBoxChildren, |
| /// ), |
| /// ), |
| /// ), |
| /// ), |
| /// body: Center( |
| /// child: RaisedButton( |
| /// child: Text('Show About Example'), |
| /// onPressed: () { |
| /// showAboutDialog( |
| /// context: context, |
| /// applicationIcon: FlutterLogo(), |
| /// applicationName: 'Show About Example', |
| /// applicationVersion: 'August 2019', |
| /// applicationLegalese: '© 2014 The Flutter Authors', |
| /// children: aboutBoxChildren, |
| /// ); |
| /// }, |
| /// ), |
| /// ), |
| /// ); |
| ///} |
| /// ``` |
| /// {@end-tool} |
| /// |
| class AboutListTile extends StatelessWidget { |
| /// Creates a list tile for showing 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. |
| const AboutListTile({ |
| Key key, |
| this.icon, |
| this.child, |
| this.applicationName, |
| this.applicationVersion, |
| this.applicationIcon, |
| this.applicationLegalese, |
| this.aboutBoxChildren, |
| this.dense, |
| }) : 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. |
| final Widget icon; |
| |
| /// 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]. |
| final Widget child; |
| |
| /// 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. |
| /// Otherwise, defaults to [Platform.resolvedExecutable]. |
| final String applicationName; |
| |
| /// The version of this build of the application. |
| /// |
| /// This string is shown under the application name in the [AboutDialog]. |
| /// |
| /// Defaults to the empty string. |
| final String applicationVersion; |
| |
| /// The icon to show next to the application name in the [AboutDialog]. |
| /// |
| /// By default no icon is shown. |
| /// |
| /// Typically this will be an [ImageIcon] widget. It should honor the |
| /// [IconTheme]'s [IconThemeData.size]. |
| /// |
| /// This is not necessarily the same as the icon shown on the drawer item |
| /// itself, which is controlled by the [icon] property. |
| final Widget applicationIcon; |
| |
| /// A string to show in small print in the [AboutDialog]. |
| /// |
| /// Typically this is a copyright notice. |
| /// |
| /// Defaults to the empty string. |
| final String applicationLegalese; |
| |
| /// 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. |
| final List<Widget> aboutBoxChildren; |
| |
| /// 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. |
| final bool dense; |
| |
| @override |
| Widget build(BuildContext context) { |
| assert(debugCheckHasMaterial(context)); |
| assert(debugCheckHasMaterialLocalizations(context)); |
| return ListTile( |
| leading: icon, |
| title: child ?? Text(MaterialLocalizations.of(context).aboutListTileTitle( |
| applicationName ?? _defaultApplicationName(context), |
| )), |
| dense: dense, |
| onTap: () { |
| showAboutDialog( |
| context: context, |
| applicationName: applicationName, |
| applicationVersion: applicationVersion, |
| applicationIcon: applicationIcon, |
| applicationLegalese: applicationLegalese, |
| children: aboutBoxChildren, |
| ); |
| }, |
| ); |
| } |
| } |
| |
| /// 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]. |
| /// |
| /// If the application has a [Drawer], consider using [AboutListTile] instead |
| /// 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]. |
| /// |
| /// The licenses shown on the [LicensePage] are those returned by the |
| /// [LicenseRegistry] API, which can be used to add more licenses to the list. |
| /// |
| /// The [context], [useRootNavigator] and [routeSettings] arguments are passed to |
| /// [showDialog], the documentation for which discusses how it is used. |
| void showAboutDialog({ |
| @required BuildContext context, |
| String applicationName, |
| String applicationVersion, |
| Widget applicationIcon, |
| String applicationLegalese, |
| List<Widget> children, |
| bool useRootNavigator = true, |
| RouteSettings routeSettings, |
| }) { |
| assert(context != null); |
| assert(useRootNavigator != null); |
| showDialog<void>( |
| context: context, |
| useRootNavigator: useRootNavigator, |
| builder: (BuildContext context) { |
| return AboutDialog( |
| applicationName: applicationName, |
| applicationVersion: applicationVersion, |
| applicationIcon: applicationIcon, |
| applicationLegalese: applicationLegalese, |
| children: children, |
| ); |
| }, |
| routeSettings: routeSettings, |
| ); |
| } |
| |
| /// Displays a [LicensePage], which shows licenses for software used by the |
| /// application. |
| /// |
| /// 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. |
| /// |
| /// If the application has a [Drawer], consider using [AboutListTile] instead |
| /// of calling this directly. |
| /// |
| /// 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. |
| void showLicensePage({ |
| @required BuildContext context, |
| String applicationName, |
| String applicationVersion, |
| Widget applicationIcon, |
| String applicationLegalese, |
| bool useRootNavigator = false, |
| }) { |
| assert(context != null); |
| assert(useRootNavigator != null); |
| Navigator.of(context, rootNavigator: useRootNavigator).push(MaterialPageRoute<void>( |
| builder: (BuildContext context) => LicensePage( |
| applicationName: applicationName, |
| applicationVersion: applicationVersion, |
| applicationIcon: applicationIcon, |
| applicationLegalese: applicationLegalese, |
| ), |
| )); |
| } |
| |
| /// 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]. |
| /// |
| /// If the application has a [Drawer], the [AboutListTile] widget can make the |
| /// 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. |
| 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. |
| const AboutDialog({ |
| Key key, |
| this.applicationName, |
| this.applicationVersion, |
| this.applicationIcon, |
| this.applicationLegalese, |
| this.children, |
| }) : super(key: key); |
| |
| /// The name of the application. |
| /// |
| /// Defaults to the value of [Title.title], if a [Title] widget can be found. |
| /// Otherwise, defaults to [Platform.resolvedExecutable]. |
| final String applicationName; |
| |
| /// The version of this build of the application. |
| /// |
| /// This string is shown under the application name. |
| /// |
| /// Defaults to the empty string. |
| final String applicationVersion; |
| |
| /// The icon to show next to the application name. |
| /// |
| /// By default no icon is shown. |
| /// |
| /// Typically this will be an [ImageIcon] widget. It should honor the |
| /// [IconTheme]'s [IconThemeData.size]. |
| final Widget applicationIcon; |
| |
| /// A string to show in small print. |
| /// |
| /// Typically this is a copyright notice. |
| /// |
| /// Defaults to the empty string. |
| final String applicationLegalese; |
| |
| /// 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. |
| final List<Widget> children; |
| |
| @override |
| Widget build(BuildContext context) { |
| assert(debugCheckHasMaterialLocalizations(context)); |
| final String name = applicationName ?? _defaultApplicationName(context); |
| final String version = applicationVersion ?? _defaultApplicationVersion(context); |
| final Widget icon = applicationIcon ?? _defaultApplicationIcon(context); |
| return AlertDialog( |
| content: ListBody( |
| children: <Widget>[ |
| Row( |
| crossAxisAlignment: CrossAxisAlignment.start, |
| children: <Widget>[ |
| if (icon != null) IconTheme(data: Theme.of(context).iconTheme, child: icon), |
| Expanded( |
| child: Padding( |
| padding: const EdgeInsets.symmetric(horizontal: 24.0), |
| child: ListBody( |
| children: <Widget>[ |
| Text(name, style: Theme.of(context).textTheme.headline5), |
| Text(version, style: Theme.of(context).textTheme.bodyText2), |
| Container(height: 18.0), |
| Text(applicationLegalese ?? '', style: Theme.of(context).textTheme.caption), |
| ], |
| ), |
| ), |
| ), |
| ], |
| ), |
| ...?children, |
| ], |
| ), |
| actions: <Widget>[ |
| FlatButton( |
| child: Text(MaterialLocalizations.of(context).viewLicensesButtonLabel), |
| onPressed: () { |
| showLicensePage( |
| context: context, |
| applicationName: applicationName, |
| applicationVersion: applicationVersion, |
| applicationIcon: applicationIcon, |
| applicationLegalese: applicationLegalese, |
| ); |
| }, |
| ), |
| FlatButton( |
| child: Text(MaterialLocalizations.of(context).closeButtonLabel), |
| onPressed: () { |
| Navigator.pop(context); |
| }, |
| ), |
| ], |
| scrollable: true, |
| ); |
| } |
| } |
| |
| /// A page that shows licenses for software used by the application. |
| /// |
| /// To show a [LicensePage], use [showLicensePage]. |
| /// |
| /// The [AboutDialog] shown by [showAboutDialog] and [AboutListTile] 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. |
| class LicensePage extends StatefulWidget { |
| /// 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. |
| /// |
| /// The licenses shown on the [LicensePage] are those returned by the |
| /// [LicenseRegistry] API, which can be used to add more licenses to the list. |
| const LicensePage({ |
| Key key, |
| this.applicationName, |
| this.applicationVersion, |
| this.applicationIcon, |
| this.applicationLegalese, |
| }) : super(key: key); |
| |
| /// The name of the application. |
| /// |
| /// Defaults to the value of [Title.title], if a [Title] widget can be found. |
| /// Otherwise, defaults to [Platform.resolvedExecutable]. |
| final String applicationName; |
| |
| /// The version of this build of the application. |
| /// |
| /// This string is shown under the application name. |
| /// |
| /// Defaults to the empty string. |
| final String applicationVersion; |
| |
| /// 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]. |
| final Widget applicationIcon; |
| |
| /// A string to show in small print. |
| /// |
| /// Typically this is a copyright notice. |
| /// |
| /// Defaults to the empty string. |
| final String applicationLegalese; |
| |
| @override |
| _LicensePageState createState() => _LicensePageState(); |
| } |
| |
| class _LicensePageState extends State<LicensePage> { |
| @override |
| void initState() { |
| super.initState(); |
| _initLicenses(); |
| } |
| |
| final List<Widget> _licenses = <Widget>[]; |
| bool _loaded = false; |
| |
| Future<void> _initLicenses() async { |
| int debugFlowId = -1; |
| assert(() { |
| final Flow flow = Flow.begin(); |
| Timeline.timeSync('_initLicenses()', () { }, flow: flow); |
| debugFlowId = flow.id; |
| return true; |
| }()); |
| await for (final LicenseEntry license in LicenseRegistry.licenses) { |
| if (!mounted) { |
| return; |
| } |
| assert(() { |
| Timeline.timeSync('_initLicenses()', () { }, flow: Flow.step(debugFlowId)); |
| return true; |
| }()); |
| final List<LicenseParagraph> paragraphs = |
| await SchedulerBinding.instance.scheduleTask<List<LicenseParagraph>>( |
| license.paragraphs.toList, |
| Priority.animation, |
| debugLabel: 'License', |
| ); |
| if (!mounted) { |
| return; |
| } |
| setState(() { |
| _licenses.add(const Padding( |
| padding: EdgeInsets.symmetric(vertical: 18.0), |
| child: Text( |
| '🍀', // That's U+1F340. Could also use U+2766 (❦) if U+1F340 doesn't work everywhere. |
| textAlign: TextAlign.center, |
| ), |
| )); |
| _licenses.add(Container( |
| decoration: const BoxDecoration( |
| border: Border(bottom: BorderSide(width: 0.0)) |
| ), |
| child: Text( |
| license.packages.join(', '), |
| style: const TextStyle(fontWeight: FontWeight.bold), |
| textAlign: TextAlign.center, |
| ), |
| )); |
| for (final LicenseParagraph paragraph in paragraphs) { |
| if (paragraph.indent == LicenseParagraph.centeredIndent) { |
| _licenses.add(Padding( |
| padding: const EdgeInsets.only(top: 16.0), |
| child: Text( |
| paragraph.text, |
| style: const TextStyle(fontWeight: FontWeight.bold), |
| textAlign: TextAlign.center, |
| ), |
| )); |
| } else { |
| assert(paragraph.indent >= 0); |
| _licenses.add(Padding( |
| padding: EdgeInsetsDirectional.only(top: 8.0, start: 16.0 * paragraph.indent), |
| child: Text(paragraph.text), |
| )); |
| } |
| } |
| }); |
| } |
| setState(() { |
| _loaded = true; |
| }); |
| assert(() { |
| Timeline.timeSync('Build scheduled', () { }, flow: Flow.end(debugFlowId)); |
| return true; |
| }()); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| assert(debugCheckHasMaterialLocalizations(context)); |
| final String name = widget.applicationName ?? _defaultApplicationName(context); |
| final String version = widget.applicationVersion ?? _defaultApplicationVersion(context); |
| final Widget icon = widget.applicationIcon ?? _defaultApplicationIcon(context); |
| final MaterialLocalizations localizations = MaterialLocalizations.of(context); |
| return Scaffold( |
| appBar: AppBar( |
| title: Text(localizations.licensesPageTitle), |
| ), |
| // All of the licenses page text is English. We don't want localized text |
| // or text direction. |
| body: Localizations.override( |
| locale: const Locale('en', 'US'), |
| context: context, |
| child: DefaultTextStyle( |
| style: Theme.of(context).textTheme.caption, |
| child: SafeArea( |
| bottom: false, |
| child: Scrollbar( |
| child: ListView( |
| padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 12.0), |
| children: <Widget>[ |
| Text(name, style: Theme.of(context).textTheme.headline5, textAlign: TextAlign.center), |
| if (icon != null) IconTheme(data: Theme.of(context).iconTheme, child: icon), |
| Text(version, style: Theme.of(context).textTheme.bodyText2, textAlign: TextAlign.center), |
| Container(height: 18.0), |
| Text(widget.applicationLegalese ?? '', style: Theme.of(context).textTheme.caption, textAlign: TextAlign.center), |
| Container(height: 18.0), |
| Text('Powered by Flutter', style: Theme.of(context).textTheme.bodyText2, textAlign: TextAlign.center), |
| Container(height: 24.0), |
| ..._licenses, |
| if (!_loaded) |
| const Padding( |
| padding: EdgeInsets.symmetric(vertical: 24.0), |
| child: Center( |
| child: CircularProgressIndicator(), |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| } |
| |
| String _defaultApplicationName(BuildContext context) { |
| // 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. |
| final Title ancestorTitle = context.findAncestorWidgetOfExactType<Title>(); |
| return ancestorTitle?.title ?? Platform.resolvedExecutable.split(Platform.pathSeparator).last; |
| } |
| |
| String _defaultApplicationVersion(BuildContext context) { |
| // TODO(ianh): Get this from the embedder somehow. |
| return ''; |
| } |
| |
| Widget _defaultApplicationIcon(BuildContext context) { |
| // TODO(ianh): Get this from the embedder somehow. |
| return null; |
| } |