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

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

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

import 'app_bar.dart';
import 'debug.dart';
import 'dialog.dart';
import 'flat_button.dart';
17
import 'list_tile.dart';
18
import 'material_localizations.dart';
Ian Hickson's avatar
Ian Hickson committed
19
import 'page.dart';
Ian Hickson's avatar
Ian Hickson committed
20
import 'progress_indicator.dart';
Ian Hickson's avatar
Ian Hickson committed
21
import 'scaffold.dart';
22
import 'scrollbar.dart';
Ian Hickson's avatar
Ian Hickson committed
23 24
import 'theme.dart';

25
/// A [ListTile] that shows an about box.
Ian Hickson's avatar
Ian Hickson committed
26
///
27 28
/// 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
29 30
///
/// The about box will include a button that shows licenses for software used by
31 32
/// 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
33 34 35
///
/// If your application does not have a [Drawer], you should provide an
/// affordance to call [showAboutDialog] or (at least) [showLicensePage].
36 37
class AboutListTile extends StatelessWidget {
  /// Creates a list tile for showing an about box.
Ian Hickson's avatar
Ian Hickson committed
38 39 40 41
  ///
  /// 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.
42
  const AboutListTile({
Ian Hickson's avatar
Ian Hickson committed
43
    Key key,
44
    this.icon = const Icon(null),
Ian Hickson's avatar
Ian Hickson committed
45 46 47 48 49
    this.child,
    this.applicationName,
    this.applicationVersion,
    this.applicationIcon,
    this.applicationLegalese,
50
    this.aboutBoxChildren,
Ian Hickson's avatar
Ian Hickson committed
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
  }) : 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.
73
  /// Otherwise, defaults to [Platform.resolvedExecutable].
Ian Hickson's avatar
Ian Hickson committed
74 75 76 77 78 79 80 81 82 83 84 85 86
  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.
  ///
87 88 89
  /// Typically this will be an [ImageIcon] widget. It should honor the
  /// [IconTheme]'s [IconThemeData.size].
  ///
Ian Hickson's avatar
Ian Hickson committed
90 91
  /// This is not necessarily the same as the icon shown on the drawer item
  /// itself, which is controlled by the [icon] property.
92
  final Widget applicationIcon;
Ian Hickson's avatar
Ian Hickson committed
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111

  /// 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;

  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterial(context));
112
    assert(debugCheckHasMaterialLocalizations(context));
113
    return ListTile(
114
      leading: icon,
Hans Muller's avatar
Hans Muller committed
115
      title: child ??
116
        Text(MaterialLocalizations.of(context).aboutListTileTitle(applicationName ?? _defaultApplicationName(context))),
117
      onTap: () {
Ian Hickson's avatar
Ian Hickson committed
118 119 120 121 122 123
        showAboutDialog(
          context: context,
          applicationName: applicationName,
          applicationVersion: applicationVersion,
          applicationIcon: applicationIcon,
          applicationLegalese: applicationLegalese,
124
          children: aboutBoxChildren,
Ian Hickson's avatar
Ian Hickson committed
125
        );
126
      },
Ian Hickson's avatar
Ian Hickson committed
127 128 129 130 131 132 133 134 135
    );
  }
}

/// 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].
///
136
/// If the application has a [Drawer], consider using [AboutListTile] instead
Ian Hickson's avatar
Ian Hickson committed
137 138 139 140
/// 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].
141 142 143
///
/// 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
144 145 146
///
/// The `context` argument is passed to [showDialog], the documentation for
/// which discusses how it is used.
Ian Hickson's avatar
Ian Hickson committed
147 148 149 150
void showAboutDialog({
  @required BuildContext context,
  String applicationName,
  String applicationVersion,
151
  Widget applicationIcon,
Ian Hickson's avatar
Ian Hickson committed
152
  String applicationLegalese,
153
  List<Widget> children,
Ian Hickson's avatar
Ian Hickson committed
154
}) {
155
  assert(context != null);
156
  showDialog<void>(
Ian Hickson's avatar
Ian Hickson committed
157
    context: context,
158
    builder: (BuildContext context) {
159
      return AboutDialog(
160 161 162 163 164 165
        applicationName: applicationName,
        applicationVersion: applicationVersion,
        applicationIcon: applicationIcon,
        applicationLegalese: applicationLegalese,
        children: children,
      );
166
    },
Ian Hickson's avatar
Ian Hickson committed
167 168 169 170 171 172 173 174
  );
}

/// Displays a [LicensePage], which shows licenses for software used by the
/// application.
///
/// The arguments correspond to the properties on [LicensePage].
///
175
/// If the application has a [Drawer], consider using [AboutListTile] instead
Ian Hickson's avatar
Ian Hickson committed
176 177 178 179
/// of calling this directly.
///
/// The [AboutDialog] shown by [showAboutDialog] includes a button that calls
/// [showLicensePage].
180 181 182
///
/// 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
183 184 185 186
void showLicensePage({
  @required BuildContext context,
  String applicationName,
  String applicationVersion,
187
  Widget applicationIcon,
188
  String applicationLegalese,
Ian Hickson's avatar
Ian Hickson committed
189
}) {
190
  assert(context != null);
191 192
  Navigator.push(context, MaterialPageRoute<void>(
    builder: (BuildContext context) => LicensePage(
193 194
      applicationName: applicationName,
      applicationVersion: applicationVersion,
195
      applicationLegalese: applicationLegalese,
196 197
    )
  ));
Ian Hickson's avatar
Ian Hickson committed
198 199 200 201 202 203 204
}

/// 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].
205
///
206
/// If the application has a [Drawer], the [AboutListTile] widget can make the
207 208 209 210 211 212 213
/// 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
214 215 216 217 218 219
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.
220
  const AboutDialog({
Ian Hickson's avatar
Ian Hickson committed
221 222 223 224 225
    Key key,
    this.applicationName,
    this.applicationVersion,
    this.applicationIcon,
    this.applicationLegalese,
226
    this.children,
Ian Hickson's avatar
Ian Hickson committed
227 228 229 230 231
  }) : super(key: key);

  /// The name of the application.
  ///
  /// Defaults to the value of [Title.title], if a [Title] widget can be found.
232
  /// Otherwise, defaults to [Platform.resolvedExecutable].
Ian Hickson's avatar
Ian Hickson committed
233 234 235 236 237 238 239 240 241 242 243 244
  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.
245 246 247 248
  ///
  /// Typically this will be an [ImageIcon] widget. It should honor the
  /// [IconTheme]'s [IconThemeData.size].
  final Widget applicationIcon;
Ian Hickson's avatar
Ian Hickson committed
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266

  /// 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) {
267
    assert(debugCheckHasMaterialLocalizations(context));
Ian Hickson's avatar
Ian Hickson committed
268 269
    final String name = applicationName ?? _defaultApplicationName(context);
    final String version = applicationVersion ?? _defaultApplicationVersion(context);
270
    final Widget icon = applicationIcon ?? _defaultApplicationIcon(context);
Ian Hickson's avatar
Ian Hickson committed
271
    List<Widget> body = <Widget>[];
272
    if (icon != null)
273 274 275
      body.add(IconTheme(data: const IconThemeData(size: 48.0), child: icon));
    body.add(Expanded(
      child: Padding(
276
        padding: const EdgeInsets.symmetric(horizontal: 24.0),
277
        child: ListBody(
Ian Hickson's avatar
Ian Hickson committed
278
          children: <Widget>[
279 280 281
            Text(name, style: Theme.of(context).textTheme.headline),
            Text(version, style: Theme.of(context).textTheme.body1),
            Container(height: 18.0),
282 283 284 285
            Text(applicationLegalese ?? '', style: Theme.of(context).textTheme.caption),
          ],
        ),
      ),
Ian Hickson's avatar
Ian Hickson committed
286 287
    ));
    body = <Widget>[
288
      Row(
Ian Hickson's avatar
Ian Hickson committed
289
        crossAxisAlignment: CrossAxisAlignment.start,
290
        children: body,
Ian Hickson's avatar
Ian Hickson committed
291 292 293 294
      ),
    ];
    if (children != null)
      body.addAll(children);
295 296 297
    return AlertDialog(
      content: SingleChildScrollView(
        child: ListBody(children: body),
Ian Hickson's avatar
Ian Hickson committed
298 299
      ),
      actions: <Widget>[
300 301
        FlatButton(
          child: Text(MaterialLocalizations.of(context).viewLicensesButtonLabel),
Ian Hickson's avatar
Ian Hickson committed
302 303 304 305 306 307
          onPressed: () {
            showLicensePage(
              context: context,
              applicationName: applicationName,
              applicationVersion: applicationVersion,
              applicationIcon: applicationIcon,
308
              applicationLegalese: applicationLegalese,
Ian Hickson's avatar
Ian Hickson committed
309
            );
310
          },
Ian Hickson's avatar
Ian Hickson committed
311
        ),
312 313
        FlatButton(
          child: Text(MaterialLocalizations.of(context).closeButtonLabel),
Ian Hickson's avatar
Ian Hickson committed
314 315
          onPressed: () {
            Navigator.pop(context);
316
          },
Ian Hickson's avatar
Ian Hickson committed
317
        ),
318
      ],
Ian Hickson's avatar
Ian Hickson committed
319 320 321 322 323 324 325
    );
  }
}

/// A page that shows licenses for software used by the application.
///
/// To show a [LicensePage], use [showLicensePage].
326
///
327
/// The [AboutDialog] shown by [showAboutDialog] and [AboutListTile] includes
328 329 330 331
/// 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.
332
class LicensePage extends StatefulWidget {
Ian Hickson's avatar
Ian Hickson committed
333 334 335 336 337
  /// 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.
338 339 340
  ///
  /// 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
341 342 343 344
  const LicensePage({
    Key key,
    this.applicationName,
    this.applicationVersion,
345
    this.applicationLegalese,
Ian Hickson's avatar
Ian Hickson committed
346 347 348 349 350
  }) : super(key: key);

  /// The name of the application.
  ///
  /// Defaults to the value of [Title.title], if a [Title] widget can be found.
351
  /// Otherwise, defaults to [Platform.resolvedExecutable].
Ian Hickson's avatar
Ian Hickson committed
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
  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;

  /// A string to show in small print.
  ///
  /// Typically this is a copyright notice.
  ///
  /// Defaults to the empty string.
  final String applicationLegalese;

368
  @override
369
  _LicensePageState createState() => _LicensePageState();
370 371 372
}

class _LicensePageState extends State<LicensePage> {
Ian Hickson's avatar
Ian Hickson committed
373 374 375 376 377 378
  @override
  void initState() {
    super.initState();
    _initLicenses();
  }

379
  final List<Widget> _licenses = <Widget>[];
Ian Hickson's avatar
Ian Hickson committed
380 381
  bool _loaded = false;

382
  Future<void> _initLicenses() async {
383 384 385 386 387 388 389
    int debugFlowId = -1;
    assert(() {
      final Flow flow = Flow.begin();
      Timeline.timeSync('_initLicenses()', () { }, flow: flow);
      debugFlowId = flow.id;
      return true;
    }());
Ian Hickson's avatar
Ian Hickson committed
390
    await for (LicenseEntry license in LicenseRegistry.licenses) {
391
      if (!mounted) {
392
        return;
393 394 395 396 397
      }
      assert(() {
        Timeline.timeSync('_initLicenses()', () { }, flow: Flow.step(debugFlowId));
        return true;
      }());
398 399
      final List<LicenseParagraph> paragraphs =
        await SchedulerBinding.instance.scheduleTask<List<LicenseParagraph>>(
400
          license.paragraphs.toList,
401 402 403
          Priority.animation,
          debugLabel: 'License',
        );
Ian Hickson's avatar
Ian Hickson committed
404
      setState(() {
405
        _licenses.add(const Padding(
406 407
          padding: EdgeInsets.symmetric(vertical: 18.0),
          child: Text(
Ian Hickson's avatar
Ian Hickson committed
408
            '🍀‬', // That's U+1F340. Could also use U+2766 (❦) if U+1F340 doesn't work everywhere.
409 410
            textAlign: TextAlign.center,
          ),
Ian Hickson's avatar
Ian Hickson committed
411
        ));
412
        _licenses.add(Container(
413
          decoration: const BoxDecoration(
414
            border: Border(bottom: BorderSide(width: 0.0))
415
          ),
416
          child: Text(
417
            license.packages.join(', '),
418
            style: const TextStyle(fontWeight: FontWeight.bold),
419 420
            textAlign: TextAlign.center,
          ),
421
        ));
422
        for (LicenseParagraph paragraph in paragraphs) {
Ian Hickson's avatar
Ian Hickson committed
423
          if (paragraph.indent == LicenseParagraph.centeredIndent) {
424
            _licenses.add(Padding(
425
              padding: const EdgeInsets.only(top: 16.0),
426
              child: Text(
Ian Hickson's avatar
Ian Hickson committed
427
                paragraph.text,
428
                style: const TextStyle(fontWeight: FontWeight.bold),
429 430
                textAlign: TextAlign.center,
              ),
Ian Hickson's avatar
Ian Hickson committed
431 432 433
            ));
          } else {
            assert(paragraph.indent >= 0);
434 435
            _licenses.add(Padding(
              padding: EdgeInsetsDirectional.only(top: 8.0, start: 16.0 * paragraph.indent),
436
              child: Text(paragraph.text),
Ian Hickson's avatar
Ian Hickson committed
437 438
            ));
          }
439
        }
Ian Hickson's avatar
Ian Hickson committed
440
      });
441
    }
Ian Hickson's avatar
Ian Hickson committed
442 443 444
    setState(() {
      _loaded = true;
    });
445 446 447 448
    assert(() {
      Timeline.timeSync('Build scheduled', () { }, flow: Flow.end(debugFlowId));
      return true;
    }());
449 450
  }

Ian Hickson's avatar
Ian Hickson committed
451 452
  @override
  Widget build(BuildContext context) {
453
    assert(debugCheckHasMaterialLocalizations(context));
454 455
    final String name = widget.applicationName ?? _defaultApplicationName(context);
    final String version = widget.applicationVersion ?? _defaultApplicationVersion(context);
Hans Muller's avatar
Hans Muller committed
456
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
457
    final List<Widget> contents = <Widget>[
458 459 460 461 462 463 464
      Text(name, style: Theme.of(context).textTheme.headline, textAlign: TextAlign.center),
      Text(version, style: Theme.of(context).textTheme.body1, 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.body1, textAlign: TextAlign.center),
      Container(height: 24.0),
465 466
    ];
    contents.addAll(_licenses);
Ian Hickson's avatar
Ian Hickson committed
467
    if (!_loaded) {
468
      contents.add(const Padding(
469 470
        padding: EdgeInsets.symmetric(vertical: 24.0),
        child: Center(
471 472
          child: CircularProgressIndicator(),
        ),
Ian Hickson's avatar
Ian Hickson committed
473 474
      ));
    }
475 476 477
    return Scaffold(
      appBar: AppBar(
        title: Text(localizations.licensesPageTitle),
Ian Hickson's avatar
Ian Hickson committed
478
      ),
Hans Muller's avatar
Hans Muller committed
479 480
      // All of the licenses page text is English. We don't want localized text
      // or text direction.
481
      body: Localizations.override(
Hans Muller's avatar
Hans Muller committed
482 483
        locale: const Locale('en', 'US'),
        context: context,
484
        child: DefaultTextStyle(
Hans Muller's avatar
Hans Muller committed
485
          style: Theme.of(context).textTheme.caption,
486 487 488 489 490 491 492
          child: SafeArea(
            bottom: false,
            child: Scrollbar(
              child: ListView(
                padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 12.0),
                children: contents,
              ),
Hans Muller's avatar
Hans Muller committed
493
            ),
494 495 496
          ),
        ),
      ),
Ian Hickson's avatar
Ian Hickson committed
497 498 499 500 501
    );
  }
}

String _defaultApplicationName(BuildContext context) {
502 503 504 505 506 507
  // 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.
508
  final Title ancestorTitle = context.ancestorWidgetOfExactType(Title);
509
  return ancestorTitle?.title ?? Platform.resolvedExecutable.split(Platform.pathSeparator).last;
Ian Hickson's avatar
Ian Hickson committed
510 511 512 513 514 515 516
}

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

517
Widget _defaultApplicationIcon(BuildContext context) {
Ian Hickson's avatar
Ian Hickson committed
518 519 520
  // TODO(ianh): Get this from the embedder somehow.
  return null;
}