Updates Sample Catalog v0.0 (#11022)

diff --git a/dev/devicelab/lib/tasks/save_catalog_screenshots.dart b/dev/devicelab/lib/tasks/save_catalog_screenshots.dart
index 6612666..f2646de 100644
--- a/dev/devicelab/lib/tasks/save_catalog_screenshots.dart
+++ b/dev/devicelab/lib/tasks/save_catalog_screenshots.dart
@@ -80,7 +80,7 @@
       throw new UploadError('upload of "$fromPath" to "$largeName" and "$smallName" failed after 2 retries');
 
     largeImage ??= await new File(fromPath).readAsBytes();
-    smallImage ??= encodePng(copyResize(decodePng(largeImage), 400));
+    smallImage ??= encodePng(copyResize(decodePng(largeImage), 300));
 
     if (!largeImageSaved)
       largeImageSaved = await save(client, largeName, largeImage);
diff --git a/examples/catalog/bin/class_index.md.template b/examples/catalog/bin/class_index.md.template
new file mode 100644
index 0000000..7411af3
--- /dev/null
+++ b/examples/catalog/bin/class_index.md.template
@@ -0,0 +1,11 @@
+---
+layout: page
+title: "@(class) Sample Apps"
+permalink: /catalog/samples/@(link)/
+---
+
+All of the sample apps listed here use the Flutter @(class) class in an interesting way. The <a href="/catalog/samples/">Sample App Catalog</a> page lists all of the sample apps.
+
+<div class="container-fluid">
+@(entries)
+</div>
diff --git a/examples/catalog/bin/entry.md.template b/examples/catalog/bin/entry.md.template
new file mode 100644
index 0000000..5875ec3
--- /dev/null
+++ b/examples/catalog/bin/entry.md.template
@@ -0,0 +1,12 @@
+  <div class="row" style="margin-bottom: 32px">
+    <a href="/catalog/samples/@(link)/">
+      <div class="col-md-3">
+        <img style="border:1px solid #000000" src="@(android screenshot)" alt="Android screenshot" class="img-responsive">
+      </div>
+   </a>
+    <div class="col-md-9">
+      <p>
+        @(summary)
+      </p>
+    </div>
+  </div>
diff --git a/examples/catalog/bin/index.md.template b/examples/catalog/bin/index.md.template
new file mode 100644
index 0000000..67e382f
--- /dev/null
+++ b/examples/catalog/bin/index.md.template
@@ -0,0 +1,11 @@
+---
+layout: page
+title: "Sample App Catalog"
+permalink: /catalog/samples/
+---
+
+Complete applications that demonstrate how to get things done with Flutter. Each sample app features a few classes or an animation, a layout, or other feature of Flutter. The samples are short, just one file and usually only one or two pages of code. They should easy to try out with your favorite IDE.
+
+<div class="container-fluid">
+@(entries)
+</div>
diff --git a/examples/catalog/bin/sample_page.dart b/examples/catalog/bin/sample_page.dart
index b790330..5096376 100644
--- a/examples/catalog/bin/sample_page.dart
+++ b/examples/catalog/bin/sample_page.dart
@@ -28,9 +28,6 @@
 Directory sampleDirectory;
 Directory testDirectory;
 Directory driverDirectory;
-String sampleTemplate;
-String screenshotTemplate;
-String screenshotDriverTemplate;
 
 void logMessage(String s) { print(s); }
 void logError(String s) { print(s); }
@@ -44,18 +41,11 @@
 }
 
 void initialize() {
-  final File sampleTemplateFile = inputFile('bin', 'sample_page.md.template');
-  final File screenshotTemplateFile = inputFile('bin', 'screenshot.dart.template');
-  final File screenshotDriverTemplateFile = inputFile('bin', 'screenshot_test.dart.template');
-
   outputDirectory = new Directory('.generated');
   sampleDirectory = new Directory('lib');
   testDirectory = new Directory('test');
   driverDirectory = new Directory('test_driver');
   outputDirectory.createSync();
-  sampleTemplate = sampleTemplateFile.readAsStringSync();
-  screenshotTemplate = screenshotTemplateFile.readAsStringSync();
-  screenshotDriverTemplate = screenshotDriverTemplateFile.readAsStringSync();
 }
 
 // Return a copy of template with each occurrence of @(foo) replaced
@@ -76,10 +66,11 @@
   logMessage('wrote $output');
 }
 
-class SampleGenerator {
-  SampleGenerator(this.sourceFile);
+class SampleInfo {
+  SampleInfo(this.sourceFile, this.commit);
 
   final File sourceFile;
+  final String commit;
   String sourceCode;
   Map<String, String> commentValues;
 
@@ -87,8 +78,19 @@
   // is used to create derived filenames like foo.md or foo.png.
   String get sourceName => basenameWithoutExtension(sourceFile.path);
 
+  // The website's link to this page will be /catalog/samples/@(link)/.
+  String get link => sourceName.replaceAll('_', '-');
+
   // The name of the widget class that defines this sample app, like 'FooSample'.
-  String get sampleClass => commentValues["sample"];
+  String get sampleClass => commentValues['sample'];
+
+  // The value of the 'Classes:' comment as a list of class names.
+  Iterable<String> get highlightedClasses {
+    final String classNames = commentValues['classes'];
+    if (classNames == null)
+      return const <String>[];
+    return classNames.split(',').map((String s) => s.trim()).where((String s) => s.isNotEmpty);
+  }
 
   // The relative import path for this sample, like '../lib/foo.dart'.
   String get importPath => '..' + Platform.pathSeparator + sourceFile.path;
@@ -133,64 +135,123 @@
     commentValues['name'] = sourceName;
     commentValues['path'] = 'examples/catalog/${sourceFile.path}';
     commentValues['source'] = sourceCode.trim();
+    commentValues['link'] = link;
+    commentValues['android screenshot'] = 'https://storage.googleapis.com/flutter-catalog/$commit/${sourceName}_small.png';
 
     return true;
   }
 }
 
-void generate() {
+void generate(String commit) {
   initialize();
 
-  final List<SampleGenerator> samples = <SampleGenerator>[];
+  final List<SampleInfo> samples = <SampleInfo>[];
   sampleDirectory.listSync().forEach((FileSystemEntity entity) {
     if (entity is File && entity.path.endsWith('.dart')) {
-      final SampleGenerator sample = new SampleGenerator(entity);
-      if (sample.initialize()) { // skip files that lack the Sample Catalog comment
-        writeExpandedTemplate(
-          outputFile(sample.sourceName + '.md'),
-          sampleTemplate,
-          sample.commentValues,
-        );
+      final SampleInfo sample = new SampleInfo(entity, commit);
+      if (sample.initialize()) // skip files that lack the Sample Catalog comment
         samples.add(sample);
-      }
     }
   });
 
   // Causes the generated imports to appear in alphabetical order.
   // Avoid complaints from flutter lint.
-  samples.sort((SampleGenerator a, SampleGenerator b) {
+  samples.sort((SampleInfo a, SampleInfo b) {
     return a.sourceName.compareTo(b.sourceName);
   });
 
+  final String entryTemplate = inputFile('bin', 'entry.md.template').readAsStringSync();
+
+  // Write the sample catalog's home page: index.md
+  final Iterable<String> entries = samples.map((SampleInfo sample) {
+    return expandTemplate(entryTemplate, sample.commentValues);
+  });
+  writeExpandedTemplate(
+    outputFile('index.md'),
+    inputFile('bin', 'index.md.template').readAsStringSync(),
+    <String, String>{
+      'entries': entries.join('\n'),
+    },
+  );
+
+  // Write the sample app files, like animated_list.md
+  for (SampleInfo sample in samples) {
+    writeExpandedTemplate(
+      outputFile(sample.sourceName + '.md'),
+      inputFile('bin', 'sample_page.md.template').readAsStringSync(),
+      sample.commentValues,
+    );
+  }
+
+  // For each unique class listened in a sample app's "Classes:" list, generate
+  // a file that's structurally the same as index.md but only contains samples
+  // that feature one class. For example AnimatedList_index.md would only
+  // include samples that had AnimatedList in their "Classes:" list.
+  final Map<String, List<SampleInfo>> classToSamples = <String, List<SampleInfo>>{};
+  for (SampleInfo sample in samples) {
+    for (String className in sample.highlightedClasses) {
+      classToSamples[className] ??= <SampleInfo>[];
+      classToSamples[className].add(sample);
+    }
+  }
+  for (String className in classToSamples.keys) {
+    final Iterable<String> entries = classToSamples[className].map((SampleInfo sample) {
+      return expandTemplate(entryTemplate, sample.commentValues);
+    });
+    writeExpandedTemplate(
+      outputFile('${className}_index.md'),
+      inputFile('bin', 'class_index.md.template').readAsStringSync(),
+      <String, String>{
+        'class': '$className',
+        'entries': entries.join('\n'),
+        'link': '${className}_index',
+      },
+    );
+  }
+
+  // Write screenshot.dart, a "test" app that displays each sample
+  // app in turn when the app is tapped.
   writeExpandedTemplate(
     outputFile('screenshot.dart', driverDirectory),
-    screenshotTemplate,
+    inputFile('bin', 'screenshot.dart.template').readAsStringSync(),
     <String, String>{
-      'imports': samples.map((SampleGenerator page) {
+      'imports': samples.map((SampleInfo page) {
         return "import '${page.importPath}' show ${page.sampleClass};\n";
       }).toList().join(),
-      'widgets': samples.map((SampleGenerator sample) {
+      'widgets': samples.map((SampleInfo sample) {
         return 'new ${sample.sampleClass}(),\n';
       }).toList().join(),
     },
   );
 
+  // Write screenshot_test.dart, a test driver for screenshot.dart
+  // that collects screenshots of each app and saves them.
   writeExpandedTemplate(
     outputFile('screenshot_test.dart', driverDirectory),
-    screenshotDriverTemplate,
+    inputFile('bin', 'screenshot_test.dart.template').readAsStringSync(),
     <String, String>{
-      'paths': samples.map((SampleGenerator sample) {
+      'paths': samples.map((SampleInfo sample) {
         return "'${outputFile(sample.sourceName + '.png').path}'";
       }).toList().join(',\n'),
     },
   );
 
-  // To generate the screenshots: flutter drive test_driver/screenshot.dart
+  // For now, the website's index.json file must be updated by hand.
+  logMessage('The following entries must appear in _data/catalog/widgets.json');
+  for (String className in classToSamples.keys)
+    logMessage('"sample": "${className}_index"');
 }
 
 void main(List<String> args) {
+  if (args.length != 1) {
+    logError(
+      'Usage (cd examples/catalog/; dart bin/sample_page.dart commit)\n'
+      'The flutter commit hash locates screenshots on storage.googleapis.com/flutter-catalog/'
+    );
+    exit(255);
+  }
   try {
-    generate();
+    generate(args[0]);
   } catch (error) {
     logError(
       'Error: sample_page.dart failed: $error\n'
@@ -199,6 +260,5 @@
     );
     exit(255);
   }
-
   exit(0);
 }
diff --git a/examples/catalog/bin/sample_page.md.template b/examples/catalog/bin/sample_page.md.template
index c3ccc78..f9470ca 100644
--- a/examples/catalog/bin/sample_page.md.template
+++ b/examples/catalog/bin/sample_page.md.template
@@ -1,18 +1,36 @@
 ---
-catalog: @(name)
+layout: page
 title: "@(title)"
-
-permalink: /catalog/@(name)/
+permalink: /catalog/samples/@(link)/
 ---
 
 @(summary)
 
+<p>
+  <div class="container-fluid">
+    <div class="row">
+      <div class="col-md-4">
+        <div class="panel panel-default">
+          <div class="panel-body" style="padding: 16px 32px;">
+            <img style="border:1px solid #000000" src="@(android screenshot)" alt="Android screenshot" class="img-responsive">
+          </div>
+          <div class="panel-footer">
+            Android screenshot
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</p>
+
 @(description)
 
-See also:
-@(see also)
+Try this app out by creating a new project with `flutter create` and replacing the contents of `lib/main.dart` with the code that follows.
 
 ```dart
 @(source)
 ```
-The source code is based on [@(path)](https://github.com/flutter/flutter/blob/master/@(path)).
+
+<h2>See also:</h2>
+@(see also)
+- The source code in [@(path)](https://github.com/flutter/flutter/blob/master/@(path)).
diff --git a/examples/catalog/bin/screenshot_test.dart.template b/examples/catalog/bin/screenshot_test.dart.template
index a924cd0..6492439 100644
--- a/examples/catalog/bin/screenshot_test.dart.template
+++ b/examples/catalog/bin/screenshot_test.dart.template
@@ -1,6 +1,7 @@
 // This file was generated using bin/screenshot_test.dart.template and
 // bin/sample_page.dart. For more information see README.md.
 
+import 'dart:async';
 import 'dart:io';
 
 import 'package:flutter_driver/flutter_driver.dart';
@@ -22,14 +23,15 @@
       final List<String> paths = <String>[
         @(paths)
       ];
-      await driver.waitUntilNoTransientCallbacks();
       for (String path in paths) {
+        await driver.waitUntilNoTransientCallbacks();
+        // TBD: when #11021 has been resolved, this shouldn't be necessary.
+        await new Future<Null>.delayed(const Duration(milliseconds: 500));
         final List<int> pixels = await driver.screenshot();
         final File file = new File(path);
         await file.writeAsBytes(pixels);
         print('wrote $file');
         await driver.tap(find.byValueKey('screenshotGestureDetector'));
-        await driver.waitUntilNoTransientCallbacks();
       }
     });
   });
diff --git a/examples/catalog/lib/animated_list.dart b/examples/catalog/lib/animated_list.dart
index 85101a7..91281ec 100644
--- a/examples/catalog/lib/animated_list.dart
+++ b/examples/catalog/lib/animated_list.dart
@@ -205,10 +205,9 @@
 
 Title: AnimatedList
 
-Summary: In this app an AnimatedList displays a list of cards which stays
+Summary: An AnimatedList that displays a list of cards which stay
 in sync with an app-specific ListModel. When an item is added to or removed
-from the model, a corresponding card items animate in or out of view
-in the animated list.
+from the model, the corresponding card animates in or out of view.
 
 Description:
 Tap an item to select it, tap it again to unselect. Tap '+' to insert at the
diff --git a/examples/catalog/lib/app_bar_bottom.dart b/examples/catalog/lib/app_bar_bottom.dart
index 308ec837..89f8434 100644
--- a/examples/catalog/lib/app_bar_bottom.dart
+++ b/examples/catalog/lib/app_bar_bottom.dart
@@ -123,14 +123,14 @@
 
 Title: AppBar with a custom bottom widget.
 
-Summary: The AppBar's bottom widget is often a TabBar however any widget with a
-PreferredSize can be used.
+Summary: Any widget with a PreferredSize can appear at the bottom of an AppBar.
 
 Description:
-In this app, the app bar's bottom widget is a TabPageSelector
-that displays the relative position of the selected page in the app's
-TabBarView. The arrow buttons in the toolbar part of the app bar select
-the previous or the next choice.
+Typically an AppBar's bottom widget is a TabBar however any widget with a
+PreferredSize can be used. In this app, the app bar's bottom widget is a
+TabPageSelector that displays the relative position of the selected page
+in the app's TabBarView. The arrow buttons in the toolbar part of the app
+bar and they select the previous or the next page.
 
 Classes: AppBar, PreferredSize, TabBarView, TabController
 
diff --git a/examples/catalog/lib/basic_app_bar.dart b/examples/catalog/lib/basic_app_bar.dart
index ed15fd9..5075d66 100644
--- a/examples/catalog/lib/basic_app_bar.dart
+++ b/examples/catalog/lib/basic_app_bar.dart
@@ -104,8 +104,7 @@
 
 Title: AppBar Basics
 
-Summary: An AppBar with a title, actions, and an overflow dropdown menu.
-One of the app's choices can be selected with an action button or the menu.
+Summary: A typcial AppBar with a title, actions, and an overflow dropdown menu.
 
 Description:
 An app that displays one of a half dozen choices with an icon and a title.
diff --git a/examples/catalog/lib/expansion_tile_sample.dart b/examples/catalog/lib/expansion_tile_sample.dart
index 01b75c7..a261217 100644
--- a/examples/catalog/lib/expansion_tile_sample.dart
+++ b/examples/catalog/lib/expansion_tile_sample.dart
@@ -98,9 +98,6 @@
 Title: ExpansionTile
 
 Summary: ExpansionTiles can used to produce two-level or multi-level lists.
-When displayed within a scrollable that creates its list items lazily,
-like a scrollable list created with `ListView.builder()`, they can be quite
-efficient, particularly for material design "expand/collapse" lists.
 
 Description:
 This app displays hierarchical data with ExpansionTiles. Tapping a tile
@@ -108,6 +105,12 @@
 its children are disposed so that the widget footprint of the list only
 reflects what's visible.
 
+When displayed within a scrollable that creates its list items lazily,
+like a scrollable list created with `ListView.builder()`, ExpansionTiles
+can be quite efficient, particularly for material design "expand/collapse"
+lists.
+
+
 Classes: ExpansionTile, ListView
 
 Sample: ExpansionTileSample
diff --git a/examples/catalog/lib/tabbed_app_bar.dart b/examples/catalog/lib/tabbed_app_bar.dart
index ab0cb6d..0b0ef58 100644
--- a/examples/catalog/lib/tabbed_app_bar.dart
+++ b/examples/catalog/lib/tabbed_app_bar.dart
@@ -85,11 +85,11 @@
 
 Title: Tabbed AppBar
 
-Summary: An AppBar can include a TabBar as its bottom widget.
+Summary: An AppBar with a TabBar as its bottom widget.
 
 Description:
 A TabBar can be used to navigate among the pages displayed in a TabBarView.
-Although a TabBar is an ordinary widget that can appear, it's most often
+Although a TabBar is an ordinary widget that can appear anywhere, it's most often
 included in the application's AppBar.
 
 Classes: AppBar, DefaultTabController, TabBar, Scaffold, TabBarView
diff --git a/examples/catalog/test_driver/README.md b/examples/catalog/test_driver/README.md
index fe96af3..36263a9 100644
--- a/examples/catalog/test_driver/README.md
+++ b/examples/catalog/test_driver/README.md
@@ -1 +1 @@
-The screenshot_test.dart file was generated by ../bin/sample_page.dart. It should not be checked in.
+The screenshot_test.dart and screenshot_test.dart files were generated by ../bin/sample_page.dart. They should not be checked in.