diff --git a/dev/devicelab/lib/tasks/save_catalog_screenshots.dart b/dev/devicelab/lib/tasks/save_catalog_screenshots.dart index 661266699f2..f2646de0d71 100644 --- a/dev/devicelab/lib/tasks/save_catalog_screenshots.dart +++ b/dev/devicelab/lib/tasks/save_catalog_screenshots.dart @@ -80,7 +80,7 @@ class Upload { 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 00000000000..7411af3ba01 --- /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 Sample App Catalog page lists all of the sample apps. + +
+@(entries) +
diff --git a/examples/catalog/bin/entry.md.template b/examples/catalog/bin/entry.md.template new file mode 100644 index 00000000000..5875ec34cb5 --- /dev/null +++ b/examples/catalog/bin/entry.md.template @@ -0,0 +1,12 @@ +
+ +
+ Android screenshot +
+
+
+

+ @(summary) +

+
+
diff --git a/examples/catalog/bin/index.md.template b/examples/catalog/bin/index.md.template new file mode 100644 index 00000000000..67e382f0576 --- /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. + +
+@(entries) +
diff --git a/examples/catalog/bin/sample_page.dart b/examples/catalog/bin/sample_page.dart index b7903307379..5096376e8aa 100644 --- a/examples/catalog/bin/sample_page.dart +++ b/examples/catalog/bin/sample_page.dart @@ -28,9 +28,6 @@ Directory outputDirectory; 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 @@ File outputFile(String name, [Directory directory]) { } 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 @@ void writeExpandedTemplate(File output, String template, Map val logMessage('wrote $output'); } -class SampleGenerator { - SampleGenerator(this.sourceFile); +class SampleInfo { + SampleInfo(this.sourceFile, this.commit); final File sourceFile; + final String commit; String sourceCode; Map commentValues; @@ -87,8 +78,19 @@ class SampleGenerator { // 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 get highlightedClasses { + final String classNames = commentValues['classes']; + if (classNames == null) + return const []; + 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 @@ class SampleGenerator { 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 samples = []; + final List samples = []; 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 entries = samples.map((SampleInfo sample) { + return expandTemplate(entryTemplate, sample.commentValues); + }); + writeExpandedTemplate( + outputFile('index.md'), + inputFile('bin', 'index.md.template').readAsStringSync(), + { + '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> classToSamples = >{}; + for (SampleInfo sample in samples) { + for (String className in sample.highlightedClasses) { + classToSamples[className] ??= []; + classToSamples[className].add(sample); + } + } + for (String className in classToSamples.keys) { + final Iterable entries = classToSamples[className].map((SampleInfo sample) { + return expandTemplate(entryTemplate, sample.commentValues); + }); + writeExpandedTemplate( + outputFile('${className}_index.md'), + inputFile('bin', 'class_index.md.template').readAsStringSync(), + { + '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(), { - '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(), { - '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 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 @@ void main(List args) { ); exit(255); } - exit(0); } diff --git a/examples/catalog/bin/sample_page.md.template b/examples/catalog/bin/sample_page.md.template index c3ccc783a2c..f9470caf653 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) +

+

+
+
+
+
+ Android screenshot +
+ +
+
+
+
+

+ @(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)). + +

See also:

+@(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 a924cd06b56..6492439d6f3 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 @@ void main() { final List paths = [ @(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.delayed(const Duration(milliseconds: 500)); final List 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 85101a74292..91281ec63dc 100644 --- a/examples/catalog/lib/animated_list.dart +++ b/examples/catalog/lib/animated_list.dart @@ -205,10 +205,9 @@ Sample Catalog 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 308ec837b52..89f8434666d 100644 --- a/examples/catalog/lib/app_bar_bottom.dart +++ b/examples/catalog/lib/app_bar_bottom.dart @@ -123,14 +123,14 @@ Sample Catalog 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 ed15fd9f904..5075d66bb2e 100644 --- a/examples/catalog/lib/basic_app_bar.dart +++ b/examples/catalog/lib/basic_app_bar.dart @@ -104,8 +104,7 @@ Sample Catalog 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 01b75c7917f..a261217124c 100644 --- a/examples/catalog/lib/expansion_tile_sample.dart +++ b/examples/catalog/lib/expansion_tile_sample.dart @@ -98,9 +98,6 @@ Sample Catalog 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 @@ expands or collapses the view of its children. When a tile is collapsed 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 ab0cb6d7caf..0b0ef58dbbb 100644 --- a/examples/catalog/lib/tabbed_app_bar.dart +++ b/examples/catalog/lib/tabbed_app_bar.dart @@ -85,11 +85,11 @@ Sample Catalog 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 fe96af377aa..36263a95f6c 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.