blob: f4a8291e0ec5b6ac6750665a2be77204b8e73d7f [file] [log] [blame]
// 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;
}