Add samples index (#359)
* add samples_index to web/ directory Co-authored-by: Thea Flowers <theaflowers@google.com> * add pub_get.dart script * build sample index in peanut post build * re-generate sample index with web demos * print more details in peanut_post_build.dart * add images for demos * run generator * update README * add animations and provider shopper as symlinks * add links to symlinked web demos * use relative paths * update cookbook images, urls, and description CSS * use relative URL for navbar link * unstage HTML files * .gitignore generated HTML files * add margin to toolbar * rename escape functions * add and update copyright headers Co-authored-by: Thea Flowers <theaflowers@google.com>
@@ -108,7 +108,7 @@ packages:
|
|||||||
name: pedantic
|
name: pedantic
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.0+1"
|
version: "1.9.0"
|
||||||
petitparser:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -169,7 +169,7 @@ packages:
|
|||||||
name: test_api
|
name: test_api
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.11"
|
version: "0.2.15"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ packages:
|
|||||||
name: pedantic
|
name: pedantic
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.0+1"
|
version: "1.9.0"
|
||||||
petitparser:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -122,7 +122,7 @@ packages:
|
|||||||
name: provider
|
name: provider
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.2"
|
version: "4.0.4"
|
||||||
quiver:
|
quiver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -176,7 +176,7 @@ packages:
|
|||||||
name: test_api
|
name: test_api
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.14"
|
version: "0.2.15"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
38
web/_tool/common.dart
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// Copyright 2020 The Flutter team. 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:io';
|
||||||
|
|
||||||
|
const ansiGreen = 32;
|
||||||
|
const ansiRed = 31;
|
||||||
|
const ansiMagenta = 35;
|
||||||
|
|
||||||
|
Future<bool> run(
|
||||||
|
String workingDir, String commandName, List<String> args) async {
|
||||||
|
var commandDescription = '`${([commandName]..addAll(args)).join(' ')}`';
|
||||||
|
|
||||||
|
logWrapped(ansiMagenta, ' Running $commandDescription');
|
||||||
|
|
||||||
|
var proc = await Process.start(
|
||||||
|
commandName,
|
||||||
|
args,
|
||||||
|
workingDirectory: Directory.current.path + '/' + workingDir,
|
||||||
|
mode: ProcessStartMode.inheritStdio,
|
||||||
|
);
|
||||||
|
|
||||||
|
var exitCode = await proc.exitCode;
|
||||||
|
|
||||||
|
if (exitCode != 0) {
|
||||||
|
logWrapped(
|
||||||
|
ansiRed, ' Failed! ($exitCode) – $workingDir – $commandDescription');
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
logWrapped(ansiGreen, ' Success! – $workingDir – $commandDescription');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void logWrapped(int code, String message) {
|
||||||
|
print('\x1B[${code}m$message\x1B[0m');
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
// Copyright 2020 The Flutter team. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file
|
||||||
|
|
||||||
// Called by https://pub.dartlang.org/packages/peanut to generate example pages
|
// Called by https://pub.dartlang.org/packages/peanut to generate example pages
|
||||||
// for hosting.
|
// for hosting.
|
||||||
@@ -10,10 +10,11 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:markdown/markdown.dart';
|
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
void main(List<String> args) {
|
import 'common.dart';
|
||||||
|
|
||||||
|
main(List<String> args) async {
|
||||||
final buildDir = args[0];
|
final buildDir = args[0];
|
||||||
final fileMap =
|
final fileMap =
|
||||||
(jsonDecode(args[1]) as Map<String, dynamic>).cast<String, String>();
|
(jsonDecode(args[1]) as Map<String, dynamic>).cast<String, String>();
|
||||||
@@ -32,21 +33,26 @@ void main(List<String> args) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final tocFile = File(p.join(buildDir, 'index.html'));
|
// Move each sample into a subdirectory, 'web'
|
||||||
if (!tocFile.existsSync()) {
|
for (var exampleDir in fileMap.values) {
|
||||||
throw StateError('$tocFile should exist!');
|
var oldDirectory = Directory(p.join(buildDir, exampleDir));
|
||||||
|
Directory(p.join(buildDir, 'web')).createSync();
|
||||||
|
oldDirectory.renameSync(p.join(buildDir, 'web', exampleDir));
|
||||||
}
|
}
|
||||||
|
|
||||||
tocFile.writeAsStringSync(
|
// Build the sample index and copy the files into this directory
|
||||||
_tocTemplate(
|
print('building the sample index...');
|
||||||
fileMap.entries.map(
|
await run('samples_index', 'pub', ['get']);
|
||||||
(entry) => _Demo(
|
await run('samples_index', 'pub', ['run', 'grinder', 'build-release']);
|
||||||
entry.key,
|
|
||||||
entry.value,
|
// Copy the contents of the samples_index/public directory to the build
|
||||||
),
|
// directory
|
||||||
),
|
logWrapped(ansiMagenta, ' Copying samples_index/public to build directory');
|
||||||
),
|
var contents = Directory(p.join('samples_index', 'public')).listSync();
|
||||||
flush: true);
|
for (var entity in contents) {
|
||||||
|
var newPath = p.join(buildDir, p.basename(entity.path));
|
||||||
|
entity.renameSync(newPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateHtml(File htmlFile, String buildDir, String exampleDir) {
|
void _updateHtml(File htmlFile, String buildDir, String exampleDir) {
|
||||||
@@ -71,55 +77,6 @@ void _updateHtml(File htmlFile, String buildDir, String exampleDir) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Demo {
|
|
||||||
final String pkgDir, buildDir;
|
|
||||||
|
|
||||||
_Demo(this.pkgDir, this.buildDir);
|
|
||||||
|
|
||||||
String get content {
|
|
||||||
final path = p.normalize(p.join(pkgDir, '..', 'README.md'));
|
|
||||||
|
|
||||||
final readmeFile = File(path);
|
|
||||||
|
|
||||||
if (!readmeFile.existsSync()) {
|
|
||||||
print(' $path – No readme!');
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
var readmeContent = readmeFile.readAsStringSync();
|
|
||||||
|
|
||||||
final tripleLineIndex = readmeContent.indexOf('\n\n\n');
|
|
||||||
var secondDoubleIndex = readmeContent.indexOf('\n\n');
|
|
||||||
|
|
||||||
if (secondDoubleIndex >= 0) {
|
|
||||||
secondDoubleIndex = readmeContent.indexOf('\n\n', secondDoubleIndex + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
final endIndices =
|
|
||||||
([tripleLineIndex, secondDoubleIndex].where((i) => i >= 0).toList()
|
|
||||||
..sort());
|
|
||||||
|
|
||||||
final endIndex =
|
|
||||||
endIndices.isEmpty ? readmeContent.length : endIndices.first;
|
|
||||||
|
|
||||||
return markdownToHtml(readmeContent.substring(0, endIndex - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
String get name => _prettyName(buildDir);
|
|
||||||
|
|
||||||
String get html => '''
|
|
||||||
<div>
|
|
||||||
<a href='$buildDir'>
|
|
||||||
<img src='${p.url.join(buildDir, 'assets/assets/preview.png')}' width="300" alt="$name">
|
|
||||||
</a>
|
|
||||||
<a class='demo-title' href='$buildDir'>$name</a>
|
|
||||||
<div>
|
|
||||||
${_indent(content, 2)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
''';
|
|
||||||
}
|
|
||||||
|
|
||||||
final _underscoreOrSlash = RegExp('_|/');
|
final _underscoreOrSlash = RegExp('_|/');
|
||||||
|
|
||||||
String _prettyName(String input) =>
|
String _prettyName(String input) =>
|
||||||
@@ -139,75 +96,9 @@ const _analytics = '''
|
|||||||
gtag('config', '$_analyticsId');
|
gtag('config', '$_analyticsId');
|
||||||
</script>''';
|
</script>''';
|
||||||
|
|
||||||
String _indent(String content, int spaces) =>
|
|
||||||
LineSplitter.split(content).join('\n' + ' ' * spaces);
|
|
||||||
|
|
||||||
const _itemsReplace = r'<!-- ITEMS -->';
|
|
||||||
|
|
||||||
const _emptyTitle = '<title></title>';
|
const _emptyTitle = '<title></title>';
|
||||||
|
|
||||||
const _standardMeta = '''
|
const _standardMeta = '''
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
$_emptyTitle''';
|
$_emptyTitle''';
|
||||||
|
|
||||||
String _tocTemplate(Iterable<_Demo> items) => '''
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
${_indent(_analytics, 2)}
|
|
||||||
$_standardMeta
|
|
||||||
<meta name="generator" content="https://pub.dartlang.org/packages/peanut">
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: "Google Sans", "Roboto", sans-serif;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
color: #1389FD;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
#toc {
|
|
||||||
text-align: left;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-self: center;
|
|
||||||
margin: 0 auto;
|
|
||||||
align-content: space-between;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
#toc > div {
|
|
||||||
width: 300px;
|
|
||||||
padding: 1rem;
|
|
||||||
margin: 0.5rem;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.125);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
#toc > div img {
|
|
||||||
display: block;
|
|
||||||
margin: 0 auto 1rem;
|
|
||||||
}
|
|
||||||
.demo-title {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
#toc > div p {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h2><a href='https://flutter.dev/web'>Flutter for web</a> samples</h2>
|
|
||||||
<a href='https://github.com/flutter/samples/tree/master/web'>Sample source code</a>
|
|
||||||
<div id="toc">
|
|
||||||
$_itemsReplace
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
'''
|
|
||||||
.replaceFirst(
|
|
||||||
_itemsReplace, _indent(items.map((d) => d.html).join('\n'), 4))
|
|
||||||
.replaceFirst(_emptyTitle, '<title>Flutter for web samples</title>');
|
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ packages:
|
|||||||
name: args
|
name: args
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.2"
|
version: "1.5.3"
|
||||||
charcode:
|
charcode:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: charcode
|
name: charcode
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.3"
|
||||||
markdown:
|
markdown:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -30,4 +30,4 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.6.4"
|
version: "1.6.4"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.1.1 <3.0.0"
|
dart: ">=2.3.0 <3.0.0"
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
// Copyright 2020 The Flutter team. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file
|
||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'common.dart';
|
||||||
|
|
||||||
const _ansiGreen = 32;
|
const _ansiGreen = 32;
|
||||||
const _ansiRed = 31;
|
const _ansiRed = 31;
|
||||||
@@ -20,14 +21,14 @@ void main() async {
|
|||||||
final results = <bool>[];
|
final results = <bool>[];
|
||||||
for (var i = 0; i < packageDirs.length; i++) {
|
for (var i = 0; i < packageDirs.length; i++) {
|
||||||
final dir = packageDirs[i];
|
final dir = packageDirs[i];
|
||||||
_logWrapped(_ansiMagenta, '\n$dir (${i + 1} of ${packageDirs.length})');
|
logWrapped(_ansiMagenta, '\n$dir (${i + 1} of ${packageDirs.length})');
|
||||||
results.add(await _run(dir, 'flutter', [
|
results.add(await run(dir, 'flutter', [
|
||||||
'pub',
|
'pub',
|
||||||
'pub',
|
'pub',
|
||||||
'upgrade',
|
'upgrade',
|
||||||
'--no-precompile',
|
'--no-precompile',
|
||||||
]));
|
]));
|
||||||
results.add(await _run(
|
results.add(await run(
|
||||||
dir,
|
dir,
|
||||||
'dartanalyzer',
|
'dartanalyzer',
|
||||||
['--fatal-infos', '--fatal-warnings', '.'],
|
['--fatal-infos', '--fatal-warnings', '.'],
|
||||||
@@ -45,45 +46,16 @@ void _printStatus(List<bool> results) {
|
|||||||
var success = (successCount == results.length);
|
var success = (successCount == results.length);
|
||||||
var pct = 100 * successCount / results.length;
|
var pct = 100 * successCount / results.length;
|
||||||
|
|
||||||
_logWrapped(success ? _ansiGreen : _ansiRed,
|
logWrapped(success ? _ansiGreen : _ansiRed,
|
||||||
'$successCount of ${results.length} (${pct.toStringAsFixed(2)}%)');
|
'$successCount of ${results.length} (${pct.toStringAsFixed(2)}%)');
|
||||||
}
|
}
|
||||||
|
|
||||||
void _logWrapped(int code, String message) {
|
|
||||||
print('\x1B[${code}m$message\x1B[0m');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> _run(
|
|
||||||
String workingDir, String commandName, List<String> args) async {
|
|
||||||
var commandDescription = '`${([commandName]..addAll(args)).join(' ')}`';
|
|
||||||
|
|
||||||
_logWrapped(_ansiMagenta, ' Running $commandDescription');
|
|
||||||
|
|
||||||
var proc = await Process.start(
|
|
||||||
commandName,
|
|
||||||
args,
|
|
||||||
workingDirectory: Directory.current.path + '/' + workingDir,
|
|
||||||
mode: ProcessStartMode.inheritStdio,
|
|
||||||
);
|
|
||||||
|
|
||||||
var exitCode = await proc.exitCode;
|
|
||||||
|
|
||||||
if (exitCode != 0) {
|
|
||||||
_logWrapped(
|
|
||||||
_ansiRed, ' Failed! ($exitCode) – $workingDir – $commandDescription');
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
_logWrapped(_ansiGreen, ' Success! – $workingDir – $commandDescription');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Iterable<String> _listPackageDirs(Directory dir) sync* {
|
Iterable<String> _listPackageDirs(Directory dir) sync* {
|
||||||
if (File('${dir.path}/pubspec.yaml').existsSync()) {
|
if (File('${dir.path}/pubspec.yaml').existsSync()) {
|
||||||
yield dir.path;
|
yield dir.path;
|
||||||
} else {
|
} else {
|
||||||
for (var subDir in dir
|
for (var subDir in dir
|
||||||
.listSync(followLinks: false)
|
.listSync(followLinks: true)
|
||||||
.whereType<Directory>()
|
.whereType<Directory>()
|
||||||
.where((d) => !Uri.file(d.path).pathSegments.last.startsWith('.'))) {
|
.where((d) => !Uri.file(d.path).pathSegments.last.startsWith('.'))) {
|
||||||
yield* _listPackageDirs(subDir);
|
yield* _listPackageDirs(subDir);
|
||||||
|
|||||||
1
web/animations
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../animations
|
||||||
@@ -7,14 +7,14 @@ packages:
|
|||||||
name: charts_common
|
name: charts_common
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.1"
|
version: "0.9.0"
|
||||||
charts_flutter:
|
charts_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: charts_flutter
|
name: charts_flutter
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.1"
|
version: "0.9.0"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -75,4 +75,4 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.8"
|
version: "2.0.8"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.2.2 <3.0.0"
|
dart: ">=2.3.0 <3.0.0"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ packages:
|
|||||||
name: charcode
|
name: charcode
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.3"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ packages:
|
|||||||
name: charcode
|
name: charcode
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.3"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# Configuration for https://pub.dartlang.org/packages/peanut
|
# Configuration for https://pub.dartlang.org/packages/peanut
|
||||||
|
|
||||||
directories:
|
directories:
|
||||||
|
- animations/web
|
||||||
|
- provider_shopper/web
|
||||||
- charts/web
|
- charts/web
|
||||||
- filipino_cuisine/web
|
- filipino_cuisine/web
|
||||||
- github_dataviz/web
|
- github_dataviz/web
|
||||||
|
|||||||
1
web/provider_shopper
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../provider_shopper
|
||||||
@@ -1,40 +1,68 @@
|
|||||||
Web samples
|
# Sample Index and Web Demos
|
||||||
|
|
||||||
## See the samples in action
|
This directory contains the index hosted at [flutter.github.io/samples][samples]
|
||||||
|
and web demos hosted with it.
|
||||||
|
|
||||||
|
## See the demos in action
|
||||||
|
|
||||||
Compiled versions of the samples are hosted at
|
Compiled versions of the samples are hosted at
|
||||||
[flutter.github.io/samples][samples].
|
[flutter.github.io/samples/#?platform=web][samples].
|
||||||
|
|
||||||
## Building samples code
|
## Building samples code
|
||||||
|
|
||||||
Go into one of the sample directories, get packages, and run using the `chrome`
|
Update Flutter and enable web support
|
||||||
device:
|
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ flutter channel dev
|
$ flutter channel dev
|
||||||
$ flutter upgrade
|
$ flutter upgrade
|
||||||
$ cd gallery
|
$ flutter config --enable-web
|
||||||
$ flutter pub get
|
```
|
||||||
|
|
||||||
|
Run the demo using the `chrome` device type:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ cd slide_puzzle
|
||||||
|
$ flutter packages get
|
||||||
$ flutter run -d chrome
|
$ flutter run -d chrome
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You should see a message printing the URL to access: `http://localhost:8080`
|
||||||
|
|
||||||
Web support is available as a technical preview and is only available in the
|
Web support is available as a technical preview and is only available in the
|
||||||
`dev` or `master` channels.
|
`dev` or `master` channels.
|
||||||
|
|
||||||
You should see a message printing the URL to access: `http://localhost:8080`
|
|
||||||
|
|
||||||
## Deploying to GitHub Pages
|
## Deploying to GitHub Pages
|
||||||
|
|
||||||
This project uses [peanut][peanut] to build the samples and commit the output
|
This project uses [peanut][peanut] to build the samples and commit the output
|
||||||
to the gh-pages branch. To deploy, run these commands in the `web/` directory:
|
to the gh-pages branch. To deploy, run these commands in the `web/` directory:
|
||||||
|
|
||||||
|
Install the peanut command:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ flutter pub global activate peanut
|
$ flutter pub global activate peanut
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify `pub get` has been run on each demo:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ dart _tool/verify_packages
|
||||||
|
```
|
||||||
|
|
||||||
|
Build all demos, along with the sample index:
|
||||||
|
|
||||||
|
```console
|
||||||
$ flutter pub global run peanut
|
$ flutter pub global run peanut
|
||||||
|
```
|
||||||
|
|
||||||
|
Deploy to GitHub Pages:
|
||||||
|
|
||||||
|
```console
|
||||||
$ git push origin gh-pages:gh-pages
|
$ git push origin gh-pages:gh-pages
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: `flutter packages get` must be run in each sample before running `peanut`.
|
## Building the sample index
|
||||||
|
|
||||||
|
See sample_index/README.md for details
|
||||||
|
|
||||||
[web]: https://flutter.dev/web
|
[web]: https://flutter.dev/web
|
||||||
[samples]: https://flutter.github.io/samples/
|
[samples]: https://flutter.github.io/samples/
|
||||||
|
|||||||
17
web/samples_index/.gitignore
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# firebase public directory
|
||||||
|
public/
|
||||||
|
|
||||||
|
# Files and directories created by pub
|
||||||
|
.dart_tool/
|
||||||
|
.packages
|
||||||
|
# Remove the following pattern if you wish to check in your lock file
|
||||||
|
pubspec.lock
|
||||||
|
|
||||||
|
# Conventional directory for build outputs
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Directory created by dartdoc
|
||||||
|
doc/api/
|
||||||
|
|
||||||
|
# All HTML files are generated by `grind generate`
|
||||||
|
web/*.html
|
||||||
3
web/samples_index/CHANGELOG.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## 1.0.0
|
||||||
|
|
||||||
|
- Initial version, created by Stagehand
|
||||||
43
web/samples_index/README.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Flutter samples index generator
|
||||||
|
|
||||||
|
This tool is used to generate the visual samples index for Flutter samples.
|
||||||
|
|
||||||
|
## Generating the index
|
||||||
|
|
||||||
|
We use [grinder](https://pub.dev/packages/grinder) to run the build tasks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ pub get
|
||||||
|
$ pub global activate grinder
|
||||||
|
$ grind build
|
||||||
|
```
|
||||||
|
|
||||||
|
This will generate the index into `./web`
|
||||||
|
|
||||||
|
## Serving the index locally
|
||||||
|
|
||||||
|
If you want to serve the index locally, you can use
|
||||||
|
[webdev](https://pub.dev/packages/webdev):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ webdev serve
|
||||||
|
```
|
||||||
|
|
||||||
|
## Publishing the index
|
||||||
|
|
||||||
|
You can build the complete index into a publishable directory using Grinder:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ grind build-release
|
||||||
|
```
|
||||||
|
|
||||||
|
This outputs the completely built index to `./public`.
|
||||||
|
|
||||||
|
## Generating cookbook content
|
||||||
|
|
||||||
|
The cookbook articles are generated using a WebDriver script that scrapes the
|
||||||
|
flutter.dev website. To run:
|
||||||
|
|
||||||
|
1. Install [ChromeDriver](https://chromedriver.chromium.org/downloads)
|
||||||
|
2. run `chromedriver --port=4444 --url-base=wd/hub --verbose`
|
||||||
|
3. run `grind scrape-cookbook`
|
||||||
14
web/samples_index/analysis_options.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Defines a default set of lint rules enforced for
|
||||||
|
# projects at Google. For details and rationale,
|
||||||
|
# see https://github.com/dart-lang/pedantic#enabled-lints.
|
||||||
|
include: package:pedantic/analysis_options.yaml
|
||||||
|
|
||||||
|
# For lint rules and documentation, see http://dart-lang.github.io/linter/lints.
|
||||||
|
# Uncomment to specify additional rules.
|
||||||
|
# linter:
|
||||||
|
# rules:
|
||||||
|
# - camel_case_types
|
||||||
|
|
||||||
|
analyzer:
|
||||||
|
# exclude:
|
||||||
|
# - path/to/excluded/files/**
|
||||||
5
web/samples_index/lib/browser.dart
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Copyright 2020 The Flutter team. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file
|
||||||
|
|
||||||
|
export 'src/search.dart';
|
||||||
76
web/samples_index/lib/cookbook.dart
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// Copyright 2020 The Flutter team. 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:io';
|
||||||
|
|
||||||
|
import 'package:samples_index/src/data.dart';
|
||||||
|
|
||||||
|
/// Utilities for generating cookbook article data
|
||||||
|
import 'package:webdriver/io.dart';
|
||||||
|
import 'package:html/parser.dart' show parse;
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
class CookbookScraper {
|
||||||
|
WebDriver _driver;
|
||||||
|
|
||||||
|
Future init() async {
|
||||||
|
_driver = await createDriver(desired: {});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future dispose() async {
|
||||||
|
await _driver.quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> fetchCookbookLinks() async {
|
||||||
|
var flutterUrl = 'https://flutter.dev';
|
||||||
|
var url = Uri.parse('$flutterUrl/docs/cookbook');
|
||||||
|
await _driver.get(url);
|
||||||
|
var pageContent = await _driver.pageSource;
|
||||||
|
var page = parse(pageContent);
|
||||||
|
var links = page.querySelectorAll('main>.container>ul>li>a');
|
||||||
|
return links.map((e) => '$flutterUrl${e.attributes["href"]}').toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Sample> getMetadata(String url) async {
|
||||||
|
await _driver.get(Uri.parse(url));
|
||||||
|
var pageContent = await _driver.pageSource;
|
||||||
|
var page = parse(pageContent);
|
||||||
|
var name = page.querySelector('main>.container>header>h1').text;
|
||||||
|
var description = page.querySelectorAll('main>.container>p').first.text;
|
||||||
|
|
||||||
|
var urlSegments = Uri.parse(url).pathSegments;
|
||||||
|
var category = urlSegments[urlSegments.length-2];
|
||||||
|
|
||||||
|
return Sample(
|
||||||
|
name: name,
|
||||||
|
description: description,
|
||||||
|
author: 'Flutter',
|
||||||
|
type: 'cookbook',
|
||||||
|
screenshots: [Screenshot(screenshotPath(url), 'Cookbook article')],
|
||||||
|
tags: ['cookbook', category],
|
||||||
|
source: url,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future takeScreenshot(String url) async {
|
||||||
|
var screenshot = await _driver.captureScreenshotAsList();
|
||||||
|
var file = File('web${screenshotPath(url)}');
|
||||||
|
await file.create(recursive: true);
|
||||||
|
await file.writeAsBytes(screenshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String screenshotPath(String url) {
|
||||||
|
var filename = parseFileName(url);
|
||||||
|
return 'images/cookbook/$filename.png';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a filename from a cookbook link. E.g.
|
||||||
|
/// `https://flutter.dev/docs/cookbook/navigation/returning-data.html` changes
|
||||||
|
/// to `returning_data.png`
|
||||||
|
String parseFileName(String link) {
|
||||||
|
var p = path.basename(link);
|
||||||
|
var dot = p.indexOf('.');
|
||||||
|
return p.substring(0, dot);
|
||||||
|
}
|
||||||
23
web/samples_index/lib/samples_index.dart
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// Copyright 2020 The Flutter team. 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:convert';
|
||||||
|
|
||||||
|
import 'package:resource/resource.dart';
|
||||||
|
|
||||||
|
import 'src/data.dart';
|
||||||
|
import 'package:checked_yaml/checked_yaml.dart';
|
||||||
|
|
||||||
|
export 'src/data.dart';
|
||||||
|
|
||||||
|
Future<List<Sample>> getSamples() async {
|
||||||
|
var yamlFile = Resource('package:samples_index/src/samples.yaml');
|
||||||
|
var cookbookFile = Resource('package:samples_index/src/cookbook.json');
|
||||||
|
var contents = await yamlFile.readAsString();
|
||||||
|
var cookbookContents = await cookbookFile.readAsString();
|
||||||
|
var index = checkedYamlDecode(contents, (m) => Index.fromJson(m),
|
||||||
|
sourceUrl: yamlFile.uri);
|
||||||
|
var cookbookIndex = Index.fromJson(json.decode(cookbookContents));
|
||||||
|
return index.samples..addAll(cookbookIndex.samples);
|
||||||
|
}
|
||||||
1329
web/samples_index/lib/src/cookbook.json
Normal file
178
web/samples_index/lib/src/data.dart
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
// Copyright 2020 The Flutter team. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file
|
||||||
|
|
||||||
|
/// Defines the data types for this project.
|
||||||
|
library data;
|
||||||
|
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:samples_index/src/util.dart' as util;
|
||||||
|
|
||||||
|
part 'data.g.dart';
|
||||||
|
|
||||||
|
/// The full list of samples
|
||||||
|
@JsonSerializable(
|
||||||
|
// Use anyMap and checked for more useful YAML parsing errors. See
|
||||||
|
// package:checked_yaml docs for details.
|
||||||
|
anyMap: true,
|
||||||
|
checked: true)
|
||||||
|
class Index {
|
||||||
|
final List<Sample> samples;
|
||||||
|
|
||||||
|
Index(this.samples);
|
||||||
|
|
||||||
|
factory Index.fromJson(Map json) => _$IndexFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$IndexToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A sample to be displayed in the app.
|
||||||
|
@JsonSerializable(anyMap: true, checked: true)
|
||||||
|
class Sample {
|
||||||
|
/// The name of the sample.
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
/// The author of the sample. Typically "Flutter"
|
||||||
|
final String author;
|
||||||
|
|
||||||
|
/// Screenshots of the sample or cookbook article. At least 1 screenshot is
|
||||||
|
/// required.
|
||||||
|
final List<Screenshot> screenshots;
|
||||||
|
|
||||||
|
/// A link to the source code or cookbook article if type is 'cookbook'.
|
||||||
|
final String source;
|
||||||
|
|
||||||
|
/// A link to this sample running in the browser.
|
||||||
|
final String web;
|
||||||
|
|
||||||
|
/// 3-5 sentences describing the sample.
|
||||||
|
final String description;
|
||||||
|
|
||||||
|
/// The difficulty level. Values are either 'beginner', 'intermediate', or
|
||||||
|
/// 'advanced'.
|
||||||
|
final String difficulty;
|
||||||
|
|
||||||
|
/// List of widgets or Flutter APIs used by the sample. e.g. "AnimatedBuilder"
|
||||||
|
/// or "ChangeNotifier".
|
||||||
|
final List<String> widgets;
|
||||||
|
|
||||||
|
/// List of packages or Flutter libraries used by the sample. third-party
|
||||||
|
/// packages.
|
||||||
|
final List<String> packages;
|
||||||
|
|
||||||
|
/// Arbitrary tags to associate with this sample.
|
||||||
|
final List<String> tags;
|
||||||
|
|
||||||
|
/// Supported platforms. Values are either 'ios', 'android', 'desktop', and
|
||||||
|
/// 'web'
|
||||||
|
final List<String> platforms;
|
||||||
|
|
||||||
|
/// Links to display on the details page
|
||||||
|
final List<Link> links;
|
||||||
|
|
||||||
|
/// The type of the sample. Supported values are either 'sample' or
|
||||||
|
/// 'cookbook'.
|
||||||
|
final String type;
|
||||||
|
|
||||||
|
/// The date this sample was created.
|
||||||
|
final DateTime date;
|
||||||
|
|
||||||
|
/// The Flutter channel this sample runs on. Either 'stable', 'dev' or
|
||||||
|
/// 'master'.
|
||||||
|
final String channel;
|
||||||
|
|
||||||
|
Sample({
|
||||||
|
this.name,
|
||||||
|
this.author,
|
||||||
|
this.screenshots,
|
||||||
|
this.source,
|
||||||
|
this.web,
|
||||||
|
this.description,
|
||||||
|
this.difficulty,
|
||||||
|
this.widgets = const [],
|
||||||
|
this.packages = const [],
|
||||||
|
this.tags = const [],
|
||||||
|
this.platforms = const [],
|
||||||
|
this.links = const [],
|
||||||
|
this.type,
|
||||||
|
this.date,
|
||||||
|
this.channel,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Sample.fromJson(Map json) => _$SampleFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$SampleToJson(this);
|
||||||
|
|
||||||
|
String get searchAttributes {
|
||||||
|
var buf = StringBuffer();
|
||||||
|
buf.write(name.toLowerCase());
|
||||||
|
buf.write(' ');
|
||||||
|
|
||||||
|
for (var tag in tags) {
|
||||||
|
buf.write('tag:${tag.toLowerCase()} ');
|
||||||
|
|
||||||
|
// Allow tags to be searched without the tag: prefix
|
||||||
|
buf.write('${tag.toLowerCase()} ');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var platform in platforms) {
|
||||||
|
buf.write('platform:$platform ');
|
||||||
|
|
||||||
|
// Allow platforms to be searched without the tag: prefix
|
||||||
|
buf.write('$platform ');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var widget in widgets) {
|
||||||
|
buf.write('widget:$widget ');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var package in packages) {
|
||||||
|
buf.write('package:$package ');
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.write('type:$type ');
|
||||||
|
|
||||||
|
return buf.toString().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
String get filename {
|
||||||
|
var nameWithoutChars = name.replaceAll(RegExp(r'[^A-Za-z0-9\-\_\ ]'), '');
|
||||||
|
var nameWithUnderscores = nameWithoutChars.replaceAll(' ', '_');
|
||||||
|
var snake = util.snakeCase(nameWithUnderscores);
|
||||||
|
var s = snake.replaceAll('__', '_');
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
String get shortDescription {
|
||||||
|
if (description.length < 64) {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
return description.substring(0, 64) + '...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A screenshot of a sample
|
||||||
|
@JsonSerializable(anyMap: true, checked: true)
|
||||||
|
class Screenshot {
|
||||||
|
final String url;
|
||||||
|
final String alt;
|
||||||
|
|
||||||
|
Screenshot(this.url, this.alt);
|
||||||
|
|
||||||
|
factory Screenshot.fromJson(Map json) => _$ScreenshotFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$ScreenshotToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An external link displayed next to a sample
|
||||||
|
@JsonSerializable(anyMap: true, checked: true)
|
||||||
|
class Link {
|
||||||
|
final String text;
|
||||||
|
final String href;
|
||||||
|
|
||||||
|
Link(this.text, this.href);
|
||||||
|
|
||||||
|
factory Link.fromJson(Map json) => _$LinkFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$LinkToJson(this);
|
||||||
|
}
|
||||||
112
web/samples_index/lib/src/data.g.dart
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of data;
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
Index _$IndexFromJson(Map json) {
|
||||||
|
return $checkedNew('Index', json, () {
|
||||||
|
final val = Index(
|
||||||
|
$checkedConvert(
|
||||||
|
json,
|
||||||
|
'samples',
|
||||||
|
(v) => (v as List)
|
||||||
|
?.map((e) => e == null ? null : Sample.fromJson(e as Map))
|
||||||
|
?.toList()),
|
||||||
|
);
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _$IndexToJson(Index instance) => <String, dynamic>{
|
||||||
|
'samples': instance.samples,
|
||||||
|
};
|
||||||
|
|
||||||
|
Sample _$SampleFromJson(Map json) {
|
||||||
|
return $checkedNew('Sample', json, () {
|
||||||
|
final val = Sample(
|
||||||
|
name: $checkedConvert(json, 'name', (v) => v as String),
|
||||||
|
author: $checkedConvert(json, 'author', (v) => v as String),
|
||||||
|
screenshots: $checkedConvert(
|
||||||
|
json,
|
||||||
|
'screenshots',
|
||||||
|
(v) => (v as List)
|
||||||
|
?.map((e) => e == null ? null : Screenshot.fromJson(e as Map))
|
||||||
|
?.toList()),
|
||||||
|
source: $checkedConvert(json, 'source', (v) => v as String),
|
||||||
|
web: $checkedConvert(json, 'web', (v) => v as String),
|
||||||
|
description: $checkedConvert(json, 'description', (v) => v as String),
|
||||||
|
difficulty: $checkedConvert(json, 'difficulty', (v) => v as String),
|
||||||
|
widgets: $checkedConvert(json, 'widgets',
|
||||||
|
(v) => (v as List)?.map((e) => e as String)?.toList()),
|
||||||
|
packages: $checkedConvert(json, 'packages',
|
||||||
|
(v) => (v as List)?.map((e) => e as String)?.toList()),
|
||||||
|
tags: $checkedConvert(
|
||||||
|
json, 'tags', (v) => (v as List)?.map((e) => e as String)?.toList()),
|
||||||
|
platforms: $checkedConvert(json, 'platforms',
|
||||||
|
(v) => (v as List)?.map((e) => e as String)?.toList()),
|
||||||
|
links: $checkedConvert(
|
||||||
|
json,
|
||||||
|
'links',
|
||||||
|
(v) => (v as List)
|
||||||
|
?.map((e) => e == null ? null : Link.fromJson(e as Map))
|
||||||
|
?.toList()),
|
||||||
|
type: $checkedConvert(json, 'type', (v) => v as String),
|
||||||
|
date: $checkedConvert(
|
||||||
|
json, 'date', (v) => v == null ? null : DateTime.parse(v as String)),
|
||||||
|
channel: $checkedConvert(json, 'channel', (v) => v as String),
|
||||||
|
);
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SampleToJson(Sample instance) => <String, dynamic>{
|
||||||
|
'name': instance.name,
|
||||||
|
'author': instance.author,
|
||||||
|
'screenshots': instance.screenshots,
|
||||||
|
'source': instance.source,
|
||||||
|
'web': instance.web,
|
||||||
|
'description': instance.description,
|
||||||
|
'difficulty': instance.difficulty,
|
||||||
|
'widgets': instance.widgets,
|
||||||
|
'packages': instance.packages,
|
||||||
|
'tags': instance.tags,
|
||||||
|
'platforms': instance.platforms,
|
||||||
|
'links': instance.links,
|
||||||
|
'type': instance.type,
|
||||||
|
'date': instance.date?.toIso8601String(),
|
||||||
|
'channel': instance.channel,
|
||||||
|
};
|
||||||
|
|
||||||
|
Screenshot _$ScreenshotFromJson(Map json) {
|
||||||
|
return $checkedNew('Screenshot', json, () {
|
||||||
|
final val = Screenshot(
|
||||||
|
$checkedConvert(json, 'url', (v) => v as String),
|
||||||
|
$checkedConvert(json, 'alt', (v) => v as String),
|
||||||
|
);
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ScreenshotToJson(Screenshot instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'url': instance.url,
|
||||||
|
'alt': instance.alt,
|
||||||
|
};
|
||||||
|
|
||||||
|
Link _$LinkFromJson(Map json) {
|
||||||
|
return $checkedNew('Link', json, () {
|
||||||
|
final val = Link(
|
||||||
|
$checkedConvert(json, 'text', (v) => v as String),
|
||||||
|
$checkedConvert(json, 'href', (v) => v as String),
|
||||||
|
);
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _$LinkToJson(Link instance) => <String, dynamic>{
|
||||||
|
'text': instance.text,
|
||||||
|
'href': instance.href,
|
||||||
|
};
|
||||||
431
web/samples_index/lib/src/samples.yaml
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
samples:
|
||||||
|
- name: Gallery
|
||||||
|
author: Flutter
|
||||||
|
screenshots:
|
||||||
|
- url: images/gallery1.png
|
||||||
|
alt: Gallery app screenshot
|
||||||
|
- url: images/gallery2.png
|
||||||
|
alt: Rally app screenshot
|
||||||
|
- url: images/gallery3.png
|
||||||
|
alt: Fortnightly app screenshot
|
||||||
|
- url: images/gallery4.png
|
||||||
|
alt: Crane app screenshot
|
||||||
|
- url: images/gallery5.png
|
||||||
|
alt: Shrine app screnshot
|
||||||
|
source: https://github.com/flutter/gallery
|
||||||
|
web: https://flutter.github.io/gallery
|
||||||
|
description: >
|
||||||
|
A collection of material design widgets, behaviors, and vignettes
|
||||||
|
implemented with Flutter.
|
||||||
|
difficulty: intermediate
|
||||||
|
widgets:
|
||||||
|
- AlertDialog
|
||||||
|
- AppBar
|
||||||
|
- BottomAppBar
|
||||||
|
- BottomNavigationBar
|
||||||
|
- BottomSheet
|
||||||
|
- Card
|
||||||
|
- Checkbox
|
||||||
|
- ChoiceChip
|
||||||
|
- CircularProgressIndicator
|
||||||
|
- Container
|
||||||
|
- CupertinoActivityIndicator
|
||||||
|
- CupertinoAlertDialog
|
||||||
|
- CupertinoButton
|
||||||
|
- CupertinoButton
|
||||||
|
- CupertinoDatePicker
|
||||||
|
- CupertinoDialogAction
|
||||||
|
- CupertinoNavigationBar
|
||||||
|
- CupertinoPageScaffold
|
||||||
|
- CupertinoSegmentedControl
|
||||||
|
- CupertinoSlider
|
||||||
|
- CupertinoSlidingSegmentedControl
|
||||||
|
- CupertinoSliverRefreshControl
|
||||||
|
- CupertinoSwitch
|
||||||
|
- CupertinoTabView
|
||||||
|
- CupertinoTextField
|
||||||
|
- CupertinoTheme
|
||||||
|
- DayPicker
|
||||||
|
- FilterChip
|
||||||
|
- FlatButton
|
||||||
|
- FloatingActionButton
|
||||||
|
- GridTile
|
||||||
|
- GridView
|
||||||
|
- Icon
|
||||||
|
- InputChip
|
||||||
|
- LayoutBuilder
|
||||||
|
- LinearProgressIndicator
|
||||||
|
- ListTile
|
||||||
|
- ListView
|
||||||
|
- MaterialBanner
|
||||||
|
- MonthPicker
|
||||||
|
- PaginatedDataTable
|
||||||
|
- PopupMenuButton
|
||||||
|
- PopupMenuItem
|
||||||
|
- Radio
|
||||||
|
- RaisedButton
|
||||||
|
- RangeSlider
|
||||||
|
- Scaffold
|
||||||
|
- SimpleDialog
|
||||||
|
- Slider
|
||||||
|
- SnackBar
|
||||||
|
- Switch
|
||||||
|
- TabBar
|
||||||
|
- TabBarView
|
||||||
|
- TextField
|
||||||
|
- TextFormField
|
||||||
|
- Tooltip
|
||||||
|
- YearPicker
|
||||||
|
packages:
|
||||||
|
- flutter/material
|
||||||
|
- flutter/cupertino
|
||||||
|
- google_fonts
|
||||||
|
- scoped_model
|
||||||
|
tags: ['intermediate', 'sample', 'gallery', 'material', 'design', 'vignettes']
|
||||||
|
platforms: ['web', 'ios', 'android']
|
||||||
|
type: demo
|
||||||
|
|
||||||
|
- name: Add to App
|
||||||
|
author: Flutter
|
||||||
|
screenshots:
|
||||||
|
- url: images/add_to_app1.png
|
||||||
|
alt: Add_to_app screenshot
|
||||||
|
- url: images/add_to_app2.png
|
||||||
|
alt: Add_to_app screenshot
|
||||||
|
source: https://github.com/flutter/samples/tree/master/add_to_app
|
||||||
|
description: >
|
||||||
|
Android and iOS projects that each import a standalone Flutter module.
|
||||||
|
difficulty: advanced
|
||||||
|
widgets:
|
||||||
|
- WidgetsFlutterBinding
|
||||||
|
- MethodChannel
|
||||||
|
packages:
|
||||||
|
- flutter/material
|
||||||
|
- flutter/services
|
||||||
|
- provider
|
||||||
|
tags: ['advanced', 'sample', 'add-to-app', 'android', 'ios', 'native', 'embedding']
|
||||||
|
platforms: ['ios', 'android']
|
||||||
|
type: sample
|
||||||
|
|
||||||
|
- name: Animations
|
||||||
|
author: Flutter
|
||||||
|
screenshots:
|
||||||
|
- url: images/animations1.png
|
||||||
|
alt: Animations sample screenshot
|
||||||
|
- url: images/animations2.png
|
||||||
|
alt: Animations sample screenshot
|
||||||
|
- url: images/animations3.png
|
||||||
|
alt: Animations sample screenshot
|
||||||
|
source: https://github.com/flutter/samples/tree/master/animations
|
||||||
|
description: >
|
||||||
|
Sample apps that showcasing Flutter's animation features.
|
||||||
|
difficulty: advanced
|
||||||
|
widgets:
|
||||||
|
- AnimatedContainer
|
||||||
|
- PageRouteBuilder
|
||||||
|
- AnimationController
|
||||||
|
- SingleTickerProviderStateMixin
|
||||||
|
- Tween
|
||||||
|
- AnimatedBuilder
|
||||||
|
- TweenSequence
|
||||||
|
- TweenSequenceItem
|
||||||
|
packages:
|
||||||
|
- flutter/material
|
||||||
|
tags: ['intermediate', 'sample', 'animation']
|
||||||
|
platforms: ['ios', 'android', 'web']
|
||||||
|
type: sample
|
||||||
|
web: web/animations
|
||||||
|
|
||||||
|
- name: Flutter Maps Firestore
|
||||||
|
author: Flutter
|
||||||
|
screenshots:
|
||||||
|
- url: images/flutter_maps_firestore1.png
|
||||||
|
alt: Flutter maps firestore screenshot
|
||||||
|
- url: images/flutter_maps_firestore2.png
|
||||||
|
alt: Flutter maps firestore screenshot
|
||||||
|
source: https://github.com/flutter/samples/tree/master/flutter_maps_firestore
|
||||||
|
description: >
|
||||||
|
A Flutter sample app that shows the end product of the Cloud Next '19 talk
|
||||||
|
Build Mobile Apps With Flutter and Google Maps.
|
||||||
|
difficulty: advanced
|
||||||
|
widgets:
|
||||||
|
- GoogleMap
|
||||||
|
packages:
|
||||||
|
- flutter/material
|
||||||
|
- cloud_firestore
|
||||||
|
- google_maps_flutter
|
||||||
|
- google_maps_webservice
|
||||||
|
tags: ['intermediate', 'sample', 'firebase', 'maps']
|
||||||
|
platforms: ['ios', 'android']
|
||||||
|
type: sample
|
||||||
|
|
||||||
|
- name: Isolate Example
|
||||||
|
author: Flutter
|
||||||
|
screenshots:
|
||||||
|
- url: images/isolate1.png
|
||||||
|
alt: Isolate example screenshot
|
||||||
|
- url: images/isolate2.png
|
||||||
|
alt: Isolate example screenshot
|
||||||
|
- url: images/isolate3.png
|
||||||
|
alt: Isolate example screenshot
|
||||||
|
source: https://github.com/flutter/samples/tree/master/isolate_example
|
||||||
|
description: >
|
||||||
|
A sample application that demonstrate best practices when using
|
||||||
|
isolates.
|
||||||
|
difficulty: intermediate
|
||||||
|
widgets:
|
||||||
|
- FutureBuilder
|
||||||
|
- AnimationController
|
||||||
|
packages:
|
||||||
|
- dart:isolate
|
||||||
|
- dart:math
|
||||||
|
tags: ['intermediate', 'sample', 'isolates', 'concurrency']
|
||||||
|
platforms: ['ios', 'android']
|
||||||
|
type: sample
|
||||||
|
|
||||||
|
- name: jsonexample
|
||||||
|
author: Flutter
|
||||||
|
screenshots:
|
||||||
|
- url: images/jsonexample1.png
|
||||||
|
alt: JSON example screenshot
|
||||||
|
- url: images/jsonexample2.png
|
||||||
|
alt: JSON example screenshot
|
||||||
|
- url: images/jsonexample3.png
|
||||||
|
alt: JSON example screenshot
|
||||||
|
source: https://github.com/flutter/samples/tree/master/jsonexample
|
||||||
|
description: >
|
||||||
|
A Flutter sample app that deserializes a set of JSON strings using three
|
||||||
|
different libraries: dart:convert, json_serializable, and built_value.
|
||||||
|
difficulty: beginner
|
||||||
|
widgets:
|
||||||
|
- Table
|
||||||
|
- TableRow
|
||||||
|
packages:
|
||||||
|
- json_serializable
|
||||||
|
- built_value
|
||||||
|
- built_collection
|
||||||
|
- json_annotation
|
||||||
|
- build_runner
|
||||||
|
- built_value_generator
|
||||||
|
tags: ['beginner', 'sample']
|
||||||
|
platforms: ['ios', 'android']
|
||||||
|
type: sample
|
||||||
|
|
||||||
|
- name: Place Tracker
|
||||||
|
author: Flutter
|
||||||
|
screenshots:
|
||||||
|
- url: images/place_tracker1.png
|
||||||
|
alt: Place Tracker screenshot
|
||||||
|
- url: images/place_tracker2.png
|
||||||
|
alt: Place Tracker screenshot
|
||||||
|
- url: images/place_tracker3.png
|
||||||
|
alt: Place Tracker screenshot
|
||||||
|
- url: images/place_tracker4.png
|
||||||
|
alt: Place Tracker screenshot
|
||||||
|
source: https://github.com/flutter/samples/tree/master/place_tracker
|
||||||
|
description: >
|
||||||
|
A sample place tracking app that uses the google_maps_flutter plugin. Keep
|
||||||
|
track of your favorite places, places you've visited, and places you want
|
||||||
|
to go. View details about these places, show them on a map, and get
|
||||||
|
directions to them.
|
||||||
|
difficulty: intermediate
|
||||||
|
widgets:
|
||||||
|
- GoogleMap
|
||||||
|
packages:
|
||||||
|
- google_maps_flutter
|
||||||
|
tags: ['intermediate', 'sample', 'json', 'serialization']
|
||||||
|
platforms: ['android']
|
||||||
|
type: sample
|
||||||
|
|
||||||
|
- name: Platform Design
|
||||||
|
author: Flutter
|
||||||
|
screenshots:
|
||||||
|
- url: images/platform_design1.png
|
||||||
|
alt: Platform Design screenshot
|
||||||
|
- url: images/platform_design2.png
|
||||||
|
alt: Platform Design screenshot
|
||||||
|
- url: images/platform_design3.png
|
||||||
|
alt: Platform Design screenshot
|
||||||
|
- url: images/platform_design4.png
|
||||||
|
alt: Platform Design screenshot
|
||||||
|
- url: images/platform_design5.png
|
||||||
|
alt: Platform Design screenshot
|
||||||
|
- url: images/platform_design6.png
|
||||||
|
alt: Platform Design screenshot
|
||||||
|
- url: images/platform_design7.png
|
||||||
|
alt: Platform Design screenshot
|
||||||
|
source: https://github.com/flutter/samples/tree/master/platform_design
|
||||||
|
description: >
|
||||||
|
A Flutter app that maximizes application code reuse while adhering to
|
||||||
|
different design patterns on Android and iOS
|
||||||
|
difficulty: advanced
|
||||||
|
widgets:
|
||||||
|
- TargetPlatform
|
||||||
|
packages:
|
||||||
|
- flutter/material
|
||||||
|
- flutter/cupertino
|
||||||
|
tags: ['advanced', 'sample', 'ios']
|
||||||
|
platforms: ['ios', 'android']
|
||||||
|
type: sample
|
||||||
|
|
||||||
|
- name: Platform View Swift
|
||||||
|
author: Flutter
|
||||||
|
screenshots:
|
||||||
|
- url: images/platform_view_swift1.png
|
||||||
|
alt: Platform View Swift screenshot
|
||||||
|
- url: images/platform_view_swift2.png
|
||||||
|
alt: Platform View Swift screenshot
|
||||||
|
source: https://github.com/flutter/samples/tree/master/platform_design
|
||||||
|
description: >
|
||||||
|
A Flutter sample app that combines a native iOS UIViewController with a
|
||||||
|
full-screen Flutter view.
|
||||||
|
difficulty: intermediate
|
||||||
|
widgets:
|
||||||
|
- MethodChannel
|
||||||
|
packages:
|
||||||
|
- flutter/material
|
||||||
|
- flutter/services
|
||||||
|
tags: ['advanced', 'sample', 'ios']
|
||||||
|
platforms: ['ios']
|
||||||
|
type: sample
|
||||||
|
|
||||||
|
- name: Provider Shopper
|
||||||
|
author: Flutter
|
||||||
|
screenshots:
|
||||||
|
- url: images/provider_shopper1.png
|
||||||
|
alt: Provider Shopper screenshot
|
||||||
|
- url: images/provider_shopper2.png
|
||||||
|
alt: Provider Shopper screenshot
|
||||||
|
- url: images/provider_shopper3.png
|
||||||
|
alt: Provider Shopper screenshot
|
||||||
|
source: https://github.com/flutter/samples/tree/master/provider_shopper
|
||||||
|
description: >
|
||||||
|
A Flutter sample app that shows a state management approach using the Provider package.
|
||||||
|
difficulty: intermediate
|
||||||
|
widgets:
|
||||||
|
- Provider
|
||||||
|
- MultiProvider
|
||||||
|
- ChangeNotifier
|
||||||
|
packages:
|
||||||
|
- provider
|
||||||
|
tags: ['intermediate', 'sample', 'provider']
|
||||||
|
platforms: ['ios', 'android', 'web']
|
||||||
|
type: sample
|
||||||
|
web: web/provider_shopper
|
||||||
|
|
||||||
|
###################
|
||||||
|
#### Web Demos ####
|
||||||
|
###################
|
||||||
|
|
||||||
|
- name: Charts
|
||||||
|
author: Flutter
|
||||||
|
screenshots:
|
||||||
|
- url: images/charts1.png
|
||||||
|
source: https://github.com/google/charts
|
||||||
|
description: >
|
||||||
|
A general-purpose charting library.
|
||||||
|
difficulty: intermediate
|
||||||
|
widgets: []
|
||||||
|
packages: []
|
||||||
|
platforms: ['ios', 'android', 'web']
|
||||||
|
tags: ['demo', 'charts']
|
||||||
|
web: web/charts
|
||||||
|
type: demo
|
||||||
|
|
||||||
|
- name: Filipino Cuisine
|
||||||
|
author: github.com/markgrancapal
|
||||||
|
screenshots:
|
||||||
|
- url: images/filipino_cuisine1.png
|
||||||
|
alt: Filipino Cuisine screenshot
|
||||||
|
source: https://github.com/markgrancapal/filipino_cuisine
|
||||||
|
description: >
|
||||||
|
Flutter Create challenge entry
|
||||||
|
difficulty: intermediate
|
||||||
|
widgets: []
|
||||||
|
packages: []
|
||||||
|
platforms: ['web']
|
||||||
|
tags: ['demo', 'flutter create']
|
||||||
|
web: web/filipino_cuisine
|
||||||
|
type: demo
|
||||||
|
|
||||||
|
- name: GitHub Dataviz
|
||||||
|
author: Larva Labs
|
||||||
|
screenshots:
|
||||||
|
- url: images/github_dataviz1.png
|
||||||
|
alt: GitHub Dataviz screenshot
|
||||||
|
source: https://github.com/flutter/samples/tree/master/web/github_dataviz
|
||||||
|
description: >
|
||||||
|
A visualization for Flutter repository data
|
||||||
|
difficulty: intermediate
|
||||||
|
widgets: []
|
||||||
|
packages: []
|
||||||
|
platforms: ['web']
|
||||||
|
tags: ['demo', 'data', 'visualization']
|
||||||
|
web: web/github_dataviz
|
||||||
|
type: demo
|
||||||
|
|
||||||
|
- name: Particle Background
|
||||||
|
author: Felix Blaschke
|
||||||
|
screenshots:
|
||||||
|
- url: images/particle_background1.png
|
||||||
|
alt: Particle Background screenshot
|
||||||
|
source: https://github.com/flutter/samples/tree/master/web/particle_background
|
||||||
|
description: >
|
||||||
|
Flutter app demonstrating package:simple_animations in action.
|
||||||
|
difficulty: intermediate
|
||||||
|
widgets: []
|
||||||
|
packages: []
|
||||||
|
platforms: ['web']
|
||||||
|
tags: ['demo', 'animation']
|
||||||
|
web: web/particle_background
|
||||||
|
type: demo
|
||||||
|
|
||||||
|
- name: Slide Puzzle
|
||||||
|
author: Kevin Moore
|
||||||
|
screenshots:
|
||||||
|
- url: images/slide_puzzle1.png
|
||||||
|
alt: Slide Puzzle screenshot
|
||||||
|
source: https://github.com/kevmoo/slide_puzzle
|
||||||
|
description: >
|
||||||
|
A slide (15) puzzle implemented in Dart and Flutter.
|
||||||
|
difficulty: advanced
|
||||||
|
widgets: []
|
||||||
|
packages: []
|
||||||
|
platforms: ['web']
|
||||||
|
tags: ['demo', 'game']
|
||||||
|
web: web/slide_puzzle
|
||||||
|
type: demo
|
||||||
|
|
||||||
|
- name: Timeflow
|
||||||
|
author: Fabian Stein
|
||||||
|
screenshots:
|
||||||
|
- url: images/timeflow1.png
|
||||||
|
alt: Timeflow screenshot
|
||||||
|
source: github.com/Fabian-Stein/timeflow
|
||||||
|
description: >
|
||||||
|
A gentle animation that provides a calming experience to stressed developers.
|
||||||
|
difficulty: advanced
|
||||||
|
widgets: []
|
||||||
|
packages: []
|
||||||
|
platforms: ['web']
|
||||||
|
tags: ['demo', 'animation']
|
||||||
|
web: web/timeflow
|
||||||
|
type: demo
|
||||||
|
|
||||||
|
- name: Vision Challenge
|
||||||
|
author: Yukkei Choi
|
||||||
|
screenshots:
|
||||||
|
- url: images/vision_challenge1.png
|
||||||
|
alt: Vision Challenge screenshot
|
||||||
|
- url: images/vision_challenge2.png
|
||||||
|
alt: Vision Challenge screenshot
|
||||||
|
source: https://github.com/flutter/samples/tree/master/web/vision_challenge
|
||||||
|
description: >
|
||||||
|
A fun game to test your color perception abilities.
|
||||||
|
difficulty: advanced
|
||||||
|
widgets: []
|
||||||
|
packages: []
|
||||||
|
platforms: ['web']
|
||||||
|
tags: ['demo', 'game']
|
||||||
|
web: web/vision_challenge
|
||||||
|
type: demo
|
||||||
68
web/samples_index/lib/src/search.dart
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// Copyright 2020 The Flutter team. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file
|
||||||
|
|
||||||
|
bool matchesQuery(String query, String sampleAttributes) {
|
||||||
|
if (query == null || query.isEmpty) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
var queryWords = query.split(' ')..removeWhere((s) => s.isEmpty);
|
||||||
|
var attributes = sampleAttributes.split(' ')..removeWhere((s) => s.isEmpty);
|
||||||
|
|
||||||
|
// Test for exact matches
|
||||||
|
if (attributes.contains(query)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test for exact matches for keywords
|
||||||
|
var matches = 0;
|
||||||
|
for (var word in queryWords) {
|
||||||
|
if (attributes.contains(word)) {
|
||||||
|
matches++;
|
||||||
|
}
|
||||||
|
if (matches == queryWords.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test for queries whose keywords are a substring of any attribute
|
||||||
|
// e.g. searching "kitten tag:cats" is a match for a sample with the
|
||||||
|
// attributes "kittens tag:cats"
|
||||||
|
matches = 0;
|
||||||
|
for (var attribute in attributes) {
|
||||||
|
for (var queryWord in queryWords) {
|
||||||
|
if (attribute.startsWith(queryWord)) {
|
||||||
|
matches++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Only return true if each search term was matched
|
||||||
|
if (matches == queryWords.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> parseHash(String hash) =>
|
||||||
|
Uri.parse(hash.substring(hash.indexOf('#') + 1)).queryParameters;
|
||||||
|
|
||||||
|
String formatHash(Map<String, String> parameters) =>
|
||||||
|
Uri().replace(queryParameters: parameters).toString();
|
||||||
|
|
||||||
|
String searchQueryFromParams(Map<String, String> params) {
|
||||||
|
var buf = StringBuffer();
|
||||||
|
if (params.containsKey('search')) {
|
||||||
|
buf.write(params['search']);
|
||||||
|
}
|
||||||
|
if (params.containsKey('type')) {
|
||||||
|
if (buf.isNotEmpty) buf.write(' ');
|
||||||
|
buf.write('type:' + params['type']);
|
||||||
|
}
|
||||||
|
if (params.containsKey('platform')) {
|
||||||
|
if (buf.isNotEmpty) buf.write(' ');
|
||||||
|
buf.write('platform:' + params['platform']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
222
web/samples_index/lib/src/templates.dart
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
// Copyright 2020 The Flutter team. 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:convert';
|
||||||
|
|
||||||
|
import 'data.dart';
|
||||||
|
import 'util.dart' as util;
|
||||||
|
|
||||||
|
String _escapeAttribute(String s) =>
|
||||||
|
HtmlEscape(HtmlEscapeMode.attribute).convert(s);
|
||||||
|
String _escapeElement(String s) => HtmlEscape(HtmlEscapeMode.element).convert(s);
|
||||||
|
|
||||||
|
String description(Sample sample) => '''
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
$_descriptionHeader
|
||||||
|
${_descriptionPage(sample)}
|
||||||
|
$_footer
|
||||||
|
</html>
|
||||||
|
''';
|
||||||
|
|
||||||
|
String index(List<Sample> samples) => '''
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
$_indexHeader
|
||||||
|
${_indexBody(samples)}
|
||||||
|
$_footer
|
||||||
|
</html>
|
||||||
|
''';
|
||||||
|
|
||||||
|
String _indexHeader = '''
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Flutter samples</title>
|
||||||
|
<link href="styles.css" rel="stylesheet" media="screen">
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<script src="packages/mdc_web/material-components-web.min.js"></script>
|
||||||
|
<script defer src="main.dart.js"></script>
|
||||||
|
</head>
|
||||||
|
''';
|
||||||
|
|
||||||
|
String _descriptionHeader = '''
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Flutter samples</title>
|
||||||
|
<link href="styles.css" rel="stylesheet" media="screen">
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<script src="packages/mdc_web/material-components-web.min.js"></script>
|
||||||
|
<script defer src="description.dart.js"></script>
|
||||||
|
</head>
|
||||||
|
''';
|
||||||
|
|
||||||
|
String _navbar = '''
|
||||||
|
<div class="navbar">
|
||||||
|
<a class="leading" href="./">
|
||||||
|
<img src="images/logos/logo_lockup_flutter_horizontal_wht.png" />
|
||||||
|
<span class="title">Samples</span>
|
||||||
|
</a>
|
||||||
|
<div class="nav-items">
|
||||||
|
<a href="https://flutter.dev/">Flutter Home</a>
|
||||||
|
<a href="https://api.flutter.dev/">API Docs</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
''';
|
||||||
|
|
||||||
|
String _footer = '''
|
||||||
|
<div class="footer">
|
||||||
|
<span>© Flutter 2020</span>
|
||||||
|
</div>
|
||||||
|
''';
|
||||||
|
|
||||||
|
String _indexBody(List<Sample> samples) => '''
|
||||||
|
<body>
|
||||||
|
<div class="content">
|
||||||
|
${util.indent(_navbar, 4)}
|
||||||
|
<div class="container">
|
||||||
|
<div class="index-header">
|
||||||
|
<h1>All Samples</h1>
|
||||||
|
<p>A curated list of Flutter samples and apps</p>
|
||||||
|
</div>
|
||||||
|
<div class="search-container">
|
||||||
|
<div id="search-bar" class="mdc-text-field mdc-text-field--with-leading-icon mdc-text-field--with-trailing-icon">
|
||||||
|
<i class="material-icons mdc-text-field__icon">search</i>
|
||||||
|
<i id="clear-button" class="material-icons mdc-text-field__icon" role="button" tabindex="0">clear</i>
|
||||||
|
<input class="mdc-text-field__input" id="text-field-hero-input">
|
||||||
|
<div class="mdc-line-ripple"></div>
|
||||||
|
<label for="text-field-hero-input" class="mdc-floating-label">Search</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="filter-menu">
|
||||||
|
<div class="filter-buttons">
|
||||||
|
<div class="mdc-chip-set mdc-chip-set--choice" role="grid">
|
||||||
|
<div class="mdc-chip mdc-chip--selected" role="row">
|
||||||
|
<div class="mdc-chip__ripple"></div>
|
||||||
|
<span role="gridcell">
|
||||||
|
<span role="button" tabindex="0" class="mdc-chip__text">All</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mdc-chip" role="row">
|
||||||
|
<div class="mdc-chip__ripple"></div>
|
||||||
|
<span role="gridcell">
|
||||||
|
<span role="button" tabindex="-1" class="mdc-chip__text">Sample</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mdc-chip" role="row">
|
||||||
|
<div class="mdc-chip__ripple"></div>
|
||||||
|
<span role="gridcell">
|
||||||
|
<span role="button" tabindex="-1" class="mdc-chip__text">Cookbook</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mdc-chip" role="row">
|
||||||
|
<div class="mdc-chip__ripple"></div>
|
||||||
|
<span role="gridcell">
|
||||||
|
<span role="button" tabindex="-1" class="mdc-chip__text">Web Demos</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="filter-end"></div>
|
||||||
|
</div>
|
||||||
|
<div class="grid">
|
||||||
|
${util.indent(_indexCards(samples), 6)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
''';
|
||||||
|
|
||||||
|
String _backgroundImage(String url) =>
|
||||||
|
_escapeAttribute('background-image: url(\'$url\');');
|
||||||
|
String _indexCards(List<Sample> samples) => samples.map(_indexCard).join();
|
||||||
|
String _indexCard(Sample sample) => '''
|
||||||
|
<div class="mdc-card demo-card mdc-elevation--z2" search-attrs="${_escapeAttribute(sample.searchAttributes)}">
|
||||||
|
<div class="mdc-card__primary-action demo-card__primary-action" tabindex="0" href="${sample.filename}.html">
|
||||||
|
<div class="mdc-card__media mdc-card__media--16-9 demo-card__media" style="${_backgroundImage(sample.screenshots.first.url)}"></div>
|
||||||
|
<div class="demo-card__label type-label">${_escapeElement(sample.type)}</div>
|
||||||
|
<div class="demo-card__primary">
|
||||||
|
<h2 class="demo-card__title mdc-typography mdc-typography--headline6">${_escapeElement(sample.name)}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="demo-card__secondary mdc-typography mdc-typography--body2">${sample.shortDescription}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
''';
|
||||||
|
|
||||||
|
String _descriptionPage(Sample sample) => '''
|
||||||
|
<body>
|
||||||
|
<div class="content">
|
||||||
|
${util.indent(_navbar, 4)}
|
||||||
|
<div class="container">
|
||||||
|
<div class="description-title-row">
|
||||||
|
<h1>${sample.name}</h1>
|
||||||
|
<div class="type-label type-label-bordered">${sample.type}</div>
|
||||||
|
</div>
|
||||||
|
<p>By ${sample.author}</p>
|
||||||
|
<div class="toolbar">
|
||||||
|
<div class="buttons">
|
||||||
|
${util.indent(_descriptionButtons(sample), 6)}
|
||||||
|
</div>
|
||||||
|
<div class="tags-container">
|
||||||
|
<div class="tags-label">
|
||||||
|
<i class="material-icons">local_offer</i>
|
||||||
|
<span>Tags</span>
|
||||||
|
</div>
|
||||||
|
<div class="tags">
|
||||||
|
${util.indent(_tags(sample), 8)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="screenshots">
|
||||||
|
${util.indent(_descriptionScreenshots(sample), 4)}
|
||||||
|
</div>
|
||||||
|
<div class="description">
|
||||||
|
${util.indent(_descriptionText(sample), 4)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
''';
|
||||||
|
|
||||||
|
String _descriptionButtons(Sample sample) {
|
||||||
|
var buf = StringBuffer();
|
||||||
|
if (sample?.web?.isNotEmpty == true) {
|
||||||
|
buf.write('''<button class="mdc-button mdc-button--outlined" onclick="window.location.href = '${sample.web}';"><span class="mdc-button__ripple"></span> Launch App</button>''');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sample.type == 'app' || sample.type == 'sample' || sample.type == 'demo') {
|
||||||
|
buf.write('''<button class="mdc-button mdc-button--outlined" onclick="window.location.href = '${sample.source}';">
|
||||||
|
<div class="mdc-button__ripple"></div>
|
||||||
|
<i class="material-icons mdc-button__icon" aria-hidden="true">code</i>
|
||||||
|
<span class="mdc-button__label">Source Code</span>
|
||||||
|
</button>''');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sample.type =='cookbook') {
|
||||||
|
buf.write('''<button class="mdc-button mdc-button--outlined" onclick="window.location.href = '${sample.source}';"> <span class="mdc-button__ripple"></span>View Recipe</button>''');
|
||||||
|
}
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _tags(Sample sample) {
|
||||||
|
var buf = StringBuffer();
|
||||||
|
for (var tag in sample.tags) {
|
||||||
|
buf.write('<a href="./#?search=tag%3A$tag">$tag</a>\n');
|
||||||
|
}
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _descriptionScreenshots(Sample sample) {
|
||||||
|
var buf = StringBuffer();
|
||||||
|
for (var screenshot in sample.screenshots) {
|
||||||
|
buf.write('<img src="${screenshot.url}" alt="${screenshot.alt}" />\n');
|
||||||
|
}
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _descriptionText(Sample sample) {
|
||||||
|
return '<p>${sample.description}</p>';
|
||||||
|
}
|
||||||
33
web/samples_index/lib/src/util.dart
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// Copyright 2020 The Flutter team. 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:convert';
|
||||||
|
|
||||||
|
String indent(String content, int spaces) =>
|
||||||
|
LineSplitter.split(content).join('\n' + ' ' * spaces);
|
||||||
|
|
||||||
|
String kebabCase(String input) => _fixCase(input, '-');
|
||||||
|
|
||||||
|
String snakeCase(String input) => _fixCase(input, '_');
|
||||||
|
|
||||||
|
final _upperCase = RegExp('[A-Z]');
|
||||||
|
|
||||||
|
String pascalCase(String input) {
|
||||||
|
if (input.isEmpty) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return input[0].toUpperCase() + input.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _fixCase(String input, String separator) =>
|
||||||
|
input.replaceAllMapped(_upperCase, (match) {
|
||||||
|
var lower = match.group(0).toLowerCase();
|
||||||
|
|
||||||
|
if (match.start > 0) {
|
||||||
|
lower = '$separator$lower';
|
||||||
|
}
|
||||||
|
|
||||||
|
return lower;
|
||||||
|
});
|
||||||
27
web/samples_index/pubspec.yaml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: samples_index
|
||||||
|
description: A visual index of Flutter samples
|
||||||
|
homepage: https://github.com/flutter/samples_index
|
||||||
|
author: Flutter Team <flutter-dev@googlegroups.com>
|
||||||
|
version: 0.0.1
|
||||||
|
environment:
|
||||||
|
sdk: '>=2.5.0 <3.0.0'
|
||||||
|
dependencies:
|
||||||
|
json_annotation: ^3.0.0
|
||||||
|
path: ^1.6.0
|
||||||
|
resource: ^2.1.6
|
||||||
|
yaml: ^2.2.0
|
||||||
|
mdc_web: ^0.5.0-pre
|
||||||
|
sass_builder: ^2.1.0
|
||||||
|
checked_yaml: ^1.0.0
|
||||||
|
webdriver: ^2.1.0
|
||||||
|
html: ^0.14.0
|
||||||
|
dev_dependencies:
|
||||||
|
grinder: ^0.8.3
|
||||||
|
pedantic: ^1.8.0
|
||||||
|
test: ^1.6.0
|
||||||
|
json_serializable: ^3.2.0
|
||||||
|
build: ^1.2.0
|
||||||
|
build_runner: ^1.7.0
|
||||||
|
build_web_compilers: ^2.7.0
|
||||||
|
tuneup: ^0.3.6
|
||||||
|
|
||||||
165
web/samples_index/test/samples_index_test.dart
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
// Copyright 2020 The Flutter team. 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:io';
|
||||||
|
|
||||||
|
import 'package:samples_index/samples_index.dart';
|
||||||
|
import 'package:samples_index/browser.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:checked_yaml/checked_yaml.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('YAML', () {
|
||||||
|
test('parsing', () async {
|
||||||
|
var file = File('test/yaml/single.yaml');
|
||||||
|
var contents = await file.readAsString();
|
||||||
|
expect(contents, isNotEmpty);
|
||||||
|
|
||||||
|
var index = checkedYamlDecode(contents, (m) => Index.fromJson(m),
|
||||||
|
sourceUrl: file.uri);
|
||||||
|
expect(index.samples, isNotEmpty);
|
||||||
|
|
||||||
|
var sample = index.samples.first;
|
||||||
|
expect(sample, isNotNull);
|
||||||
|
expect(sample.name, 'Kittens');
|
||||||
|
expect(sample.screenshots, hasLength(2));
|
||||||
|
expect(sample.source, 'http://github.com/johnpryan/kittens');
|
||||||
|
expect(sample.description, 'A sample kitten app');
|
||||||
|
expect(sample.difficulty, 'beginner');
|
||||||
|
expect(sample.widgets, hasLength(2));
|
||||||
|
expect(sample.widgets.first, 'AnimatedBuilder');
|
||||||
|
expect(sample.packages, hasLength(2));
|
||||||
|
expect(sample.packages.first, 'json_serializable');
|
||||||
|
expect(sample.tags, hasLength(3));
|
||||||
|
expect(sample.tags[1], 'kittens');
|
||||||
|
expect(sample.platforms, hasLength(3));
|
||||||
|
expect(sample.links, hasLength(2));
|
||||||
|
expect(sample.links[1].text, 'author');
|
||||||
|
expect(sample.links[1].href, 'http://jpryan.me');
|
||||||
|
expect(sample.type, 'sample');
|
||||||
|
expect(sample.date, DateTime.parse('2019-12-15T02:59:43.1Z'));
|
||||||
|
expect(sample.channel, 'stable');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('bad yaml', () async {
|
||||||
|
var file = File('test/yaml/bad.yaml');
|
||||||
|
var contents = await file.readAsString();
|
||||||
|
expect(contents, isNotEmpty);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => checkedYamlDecode(contents, (m) => Index.fromJson(m),
|
||||||
|
sourceUrl: file.uri),
|
||||||
|
throwsA(predicate((e) =>
|
||||||
|
e is ParsedYamlException &&
|
||||||
|
e.message.endsWith('Unsupported value for "name".'))));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('searching', () {
|
||||||
|
test('search attributes', () async {
|
||||||
|
var file = File('test/yaml/single.yaml');
|
||||||
|
var contents = await file.readAsString();
|
||||||
|
expect(contents, isNotEmpty);
|
||||||
|
|
||||||
|
var index = checkedYamlDecode(contents, (m) => Index.fromJson(m),
|
||||||
|
sourceUrl: file.uri);
|
||||||
|
var sample = index.samples.first;
|
||||||
|
expect(
|
||||||
|
sample.searchAttributes.split(' '),
|
||||||
|
containsAll([
|
||||||
|
'kittens',
|
||||||
|
'tag:beginner',
|
||||||
|
'tag:kittens',
|
||||||
|
'tag:cats',
|
||||||
|
|
||||||
|
// Verify tags are searchable without the prefix
|
||||||
|
'beginner',
|
||||||
|
'kittens',
|
||||||
|
'cats',
|
||||||
|
|
||||||
|
'platform:web',
|
||||||
|
'platform:ios',
|
||||||
|
'platform:android',
|
||||||
|
|
||||||
|
// Verify platforms are searchable without the prefix
|
||||||
|
'web',
|
||||||
|
'ios',
|
||||||
|
'android',
|
||||||
|
|
||||||
|
'widget:AnimatedBuilder',
|
||||||
|
'widget:FutureBuilder',
|
||||||
|
'package:json_serializable',
|
||||||
|
'package:path',
|
||||||
|
|
||||||
|
'type:sample',
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('matchesQuery', () {
|
||||||
|
var attributes = 'kittens '
|
||||||
|
'tag:beginner '
|
||||||
|
'tag:kittens '
|
||||||
|
'tag:cats '
|
||||||
|
'platform:web '
|
||||||
|
'platform:ios '
|
||||||
|
'platform:android '
|
||||||
|
'widget:AnimatedBuilder '
|
||||||
|
'widget:FutureBuilder '
|
||||||
|
'package:json_serializable '
|
||||||
|
'package:path';
|
||||||
|
|
||||||
|
// Test if various queries match these attributes
|
||||||
|
expect(matchesQuery('foo', attributes), false);
|
||||||
|
expect(matchesQuery('kittens', attributes), true);
|
||||||
|
expect(matchesQuery('tag:cats', attributes), true);
|
||||||
|
expect(matchesQuery('tag:dogs', attributes), false);
|
||||||
|
expect(matchesQuery('package:path', attributes), true);
|
||||||
|
|
||||||
|
// Test if partial queries match these attributes
|
||||||
|
expect(matchesQuery('kitten', attributes), true);
|
||||||
|
|
||||||
|
// Test if multiple keywords match
|
||||||
|
expect(matchesQuery('kittens tag:cats', attributes), true);
|
||||||
|
expect(matchesQuery('kitten tag:cats', attributes), true);
|
||||||
|
expect(matchesQuery('tag:beginner dogs', attributes), false);
|
||||||
|
expect(matchesQuery('asdf ', attributes), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Hash parameters', () {
|
||||||
|
test('can be parsed', () {
|
||||||
|
expect(parseHash('#?search=kittens&platform=web'),
|
||||||
|
containsPair('search', 'kittens'));
|
||||||
|
expect(parseHash('#?search=kittens&platform=web'),
|
||||||
|
containsPair('platform', 'web'));
|
||||||
|
expect(parseHash('#?type=sample'), containsPair('type', 'sample'));
|
||||||
|
expect(parseHash('#?type=cookbook'), containsPair('type', 'cookbook'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can be set', () {
|
||||||
|
expect(
|
||||||
|
formatHash({
|
||||||
|
'search': 'kittens',
|
||||||
|
'platform': 'web',
|
||||||
|
}),
|
||||||
|
equals('?search=kittens&platform=web'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('creates search attributes', () {
|
||||||
|
expect(
|
||||||
|
searchQueryFromParams({
|
||||||
|
'search': 'kittens',
|
||||||
|
'platform': 'web',
|
||||||
|
'type': 'sample',
|
||||||
|
}),
|
||||||
|
equals('kittens type:sample platform:web'));
|
||||||
|
expect(
|
||||||
|
searchQueryFromParams({
|
||||||
|
'search': 'kittens',
|
||||||
|
}),
|
||||||
|
equals('kittens'));
|
||||||
|
expect(searchQueryFromParams({}), equals(''));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
27
web/samples_index/test/yaml/bad.yaml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
samples:
|
||||||
|
# Bad type, should be string, but it's a number.
|
||||||
|
- name: 42
|
||||||
|
screenshots:
|
||||||
|
- url: https://placekitten.com/500/500
|
||||||
|
alt: a kitten
|
||||||
|
- url: https://placekitten.com/400/400
|
||||||
|
alt: another kitten
|
||||||
|
source: http://github.com/johnpryan/kittens
|
||||||
|
description: A sample kitten app
|
||||||
|
difficulty: beginner
|
||||||
|
widgets:
|
||||||
|
- AnimatedBuilder
|
||||||
|
- FutureBuilder
|
||||||
|
packages:
|
||||||
|
- json_serializable
|
||||||
|
- path
|
||||||
|
tags: ['beginner', 'kittens', 'cats']
|
||||||
|
platforms: ['web', 'ios', 'android']
|
||||||
|
links:
|
||||||
|
- text: inspiration
|
||||||
|
href: https://apps.apple.com/us/app/neko-atsume-kitty-collector/id923917775
|
||||||
|
- text: author
|
||||||
|
href: http://jpryan.me
|
||||||
|
type: sample # sample, app, or cookbook
|
||||||
|
date: 2019-12-15T02:59:43.1Z
|
||||||
|
channel: stable
|
||||||
26
web/samples_index/test/yaml/single.yaml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
samples:
|
||||||
|
- name: Kittens
|
||||||
|
screenshots:
|
||||||
|
- url: https://placekitten.com/500/500
|
||||||
|
alt: a kitten
|
||||||
|
- url: https://placekitten.com/400/400
|
||||||
|
alt: another kitten
|
||||||
|
source: http://github.com/johnpryan/kittens
|
||||||
|
description: A sample kitten app
|
||||||
|
difficulty: beginner
|
||||||
|
widgets:
|
||||||
|
- AnimatedBuilder
|
||||||
|
- FutureBuilder
|
||||||
|
packages:
|
||||||
|
- json_serializable
|
||||||
|
- path
|
||||||
|
tags: ['beginner', 'kittens', 'cats']
|
||||||
|
platforms: ['web', 'ios', 'android']
|
||||||
|
links:
|
||||||
|
- text: inspiration
|
||||||
|
href: https://apps.apple.com/us/app/neko-atsume-kitty-collector/id923917775
|
||||||
|
- text: author
|
||||||
|
href: http://jpryan.me
|
||||||
|
type: sample # sample, app, or cookbook
|
||||||
|
date: 2019-12-15T02:59:43.1Z
|
||||||
|
channel: stable
|
||||||
94
web/samples_index/tool/grind.dart
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
// Copyright 2020 The Flutter team. 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:io';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:grinder/grinder.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:samples_index/samples_index.dart';
|
||||||
|
import 'package:samples_index/src/templates.dart' as templates;
|
||||||
|
import 'package:samples_index/cookbook.dart';
|
||||||
|
|
||||||
|
void main(args) => grind(args);
|
||||||
|
|
||||||
|
@Task('Run tests in the VM')
|
||||||
|
void testCli() async => await TestRunner().testAsync(platformSelector: 'vm');
|
||||||
|
|
||||||
|
@Task()
|
||||||
|
void analyze() {
|
||||||
|
PubApp.local('tuneup')..run(['check']);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Task('deploy')
|
||||||
|
@Depends(analyze, testCli, generate, buildRelease)
|
||||||
|
void deploy() {
|
||||||
|
print('All tasks completed. To deploy to Firebase, run:');
|
||||||
|
print('');
|
||||||
|
print(' firebase deploy');
|
||||||
|
print('');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Task('Run build_runner to public/ directory')
|
||||||
|
Future buildRelease() async {
|
||||||
|
var app = PubApp.local('build_runner');
|
||||||
|
await app.runAsync('build --release --output web:public'.split(' ').toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@DefaultTask('Build the project.')
|
||||||
|
@Depends(clean)
|
||||||
|
Future generate() async {
|
||||||
|
var samples = await getSamples();
|
||||||
|
print('Generating index for ${samples.length} samples...');
|
||||||
|
var outputFile = File('web/index.html');
|
||||||
|
await outputFile.create(recursive: true);
|
||||||
|
await outputFile.writeAsString(templates.index(samples));
|
||||||
|
var futures = <Future>[];
|
||||||
|
for (var sample in samples) {
|
||||||
|
var file = File('web/${sample.filename}.html');
|
||||||
|
var future = file.create(recursive: true).then((_) async {
|
||||||
|
await file.writeAsString(templates.description(sample));
|
||||||
|
});
|
||||||
|
futures.add(future);
|
||||||
|
}
|
||||||
|
await Future.wait(futures);
|
||||||
|
print('Generated index for ${samples.length} samples.');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Task('Scrape the cookbook for images and descriptions')
|
||||||
|
Future scrapeCookbook() async {
|
||||||
|
var driver = await Process.start(
|
||||||
|
'chromedriver', ['--port=4444', '--url-base=wd/hub', '--verbose']);
|
||||||
|
driver.stdout.pipe(stdout);
|
||||||
|
driver.stderr.pipe(stderr);
|
||||||
|
var scraper = CookbookScraper();
|
||||||
|
await scraper.init();
|
||||||
|
var links = await scraper.fetchCookbookLinks();
|
||||||
|
print('Scraping ${links.length} cookbook articles');
|
||||||
|
var allSamples = <Sample>[];
|
||||||
|
for (var link in links) {
|
||||||
|
allSamples.add(await scraper.getMetadata(link));
|
||||||
|
await scraper.takeScreenshot(link);
|
||||||
|
}
|
||||||
|
var file = File('lib/src/cookbook.json');
|
||||||
|
await file.create();
|
||||||
|
var encoder = JsonEncoder.withIndent('\t');
|
||||||
|
await file.writeAsString(encoder.convert(Index(allSamples)));
|
||||||
|
await scraper.dispose();
|
||||||
|
var killed = driver.kill();
|
||||||
|
if (!killed) {
|
||||||
|
print('failed to kill chromedriver process');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Task('remove generated HTML files')
|
||||||
|
Future clean() async {
|
||||||
|
var tasks = <Future>[];
|
||||||
|
await for (var file in Directory('web').list(recursive: true)) {
|
||||||
|
if (path.extension(file.path) == '.html') {
|
||||||
|
tasks.add(file.delete());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Future.wait(tasks);
|
||||||
|
}
|
||||||
10
web/samples_index/web/description.dart
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import 'dart:html';
|
||||||
|
|
||||||
|
import 'package:mdc_web/mdc_web.dart';
|
||||||
|
|
||||||
|
InputElement searchInput;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
querySelectorAll('.mdc-card__primary-action')
|
||||||
|
.forEach((el) => MDCRipple(el));
|
||||||
|
}
|
||||||
BIN
web/samples_index/web/images/add_to_app1.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
web/samples_index/web/images/add_to_app2.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
web/samples_index/web/images/animations1.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
web/samples_index/web/images/animations2.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
web/samples_index/web/images/animations3.png
Normal file
|
After Width: | Height: | Size: 574 KiB |
BIN
web/samples_index/web/images/charts1.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
web/samples_index/web/images/cookbook/animated-container.png
Normal file
|
After Width: | Height: | Size: 706 KiB |
BIN
web/samples_index/web/images/cookbook/authenticated-requests.png
Normal file
|
After Width: | Height: | Size: 583 KiB |
BIN
web/samples_index/web/images/cookbook/background-parsing.png
Normal file
|
After Width: | Height: | Size: 646 KiB |
BIN
web/samples_index/web/images/cookbook/basic-list.png
Normal file
|
After Width: | Height: | Size: 462 KiB |
BIN
web/samples_index/web/images/cookbook/cached-images.png
Normal file
|
After Width: | Height: | Size: 552 KiB |
BIN
web/samples_index/web/images/cookbook/dismissible.png
Normal file
|
After Width: | Height: | Size: 584 KiB |
BIN
web/samples_index/web/images/cookbook/drawer.png
Normal file
|
After Width: | Height: | Size: 592 KiB |
BIN
web/samples_index/web/images/cookbook/error-reporting.png
Normal file
|
After Width: | Height: | Size: 638 KiB |
BIN
web/samples_index/web/images/cookbook/fading-in-images.png
Normal file
|
After Width: | Height: | Size: 555 KiB |
BIN
web/samples_index/web/images/cookbook/fetch-data.png
Normal file
|
After Width: | Height: | Size: 602 KiB |
BIN
web/samples_index/web/images/cookbook/finders.png
Normal file
|
After Width: | Height: | Size: 613 KiB |
BIN
web/samples_index/web/images/cookbook/floating-app-bar.png
Normal file
|
After Width: | Height: | Size: 731 KiB |
BIN
web/samples_index/web/images/cookbook/focus.png
Normal file
|
After Width: | Height: | Size: 648 KiB |
BIN
web/samples_index/web/images/cookbook/fonts.png
Normal file
|
After Width: | Height: | Size: 597 KiB |
BIN
web/samples_index/web/images/cookbook/grid-lists.png
Normal file
|
After Width: | Height: | Size: 538 KiB |
BIN
web/samples_index/web/images/cookbook/handling-taps.png
Normal file
|
After Width: | Height: | Size: 550 KiB |
BIN
web/samples_index/web/images/cookbook/hero-animations.png
Normal file
|
After Width: | Height: | Size: 608 KiB |
BIN
web/samples_index/web/images/cookbook/horizontal-list.png
Normal file
|
After Width: | Height: | Size: 461 KiB |
BIN
web/samples_index/web/images/cookbook/introduction.png
Normal file
|
After Width: | Height: | Size: 688 KiB |
BIN
web/samples_index/web/images/cookbook/key-value.png
Normal file
|
After Width: | Height: | Size: 582 KiB |
BIN
web/samples_index/web/images/cookbook/long-lists.png
Normal file
|
After Width: | Height: | Size: 555 KiB |
BIN
web/samples_index/web/images/cookbook/mixed-list.png
Normal file
|
After Width: | Height: | Size: 600 KiB |
BIN
web/samples_index/web/images/cookbook/mocking.png
Normal file
|
After Width: | Height: | Size: 644 KiB |
BIN
web/samples_index/web/images/cookbook/named-routes.png
Normal file
|
After Width: | Height: | Size: 619 KiB |
|
After Width: | Height: | Size: 669 KiB |
BIN
web/samples_index/web/images/cookbook/navigation-basics.png
Normal file
|
After Width: | Height: | Size: 612 KiB |
BIN
web/samples_index/web/images/cookbook/network-image.png
Normal file
|
After Width: | Height: | Size: 532 KiB |
BIN
web/samples_index/web/images/cookbook/opacity-animation.png
Normal file
|
After Width: | Height: | Size: 598 KiB |
BIN
web/samples_index/web/images/cookbook/orientation.png
Normal file
|
After Width: | Height: | Size: 602 KiB |
BIN
web/samples_index/web/images/cookbook/package-fonts.png
Normal file
|
After Width: | Height: | Size: 590 KiB |
BIN
web/samples_index/web/images/cookbook/page-route-animation.png
Normal file
|
After Width: | Height: | Size: 626 KiB |
BIN
web/samples_index/web/images/cookbook/passing-data.png
Normal file
|
After Width: | Height: | Size: 644 KiB |
BIN
web/samples_index/web/images/cookbook/physics-simulation.png
Normal file
|
After Width: | Height: | Size: 590 KiB |
BIN
web/samples_index/web/images/cookbook/picture-using-camera.png
Normal file
|
After Width: | Height: | Size: 604 KiB |
BIN
web/samples_index/web/images/cookbook/play-video.png
Normal file
|
After Width: | Height: | Size: 616 KiB |
BIN
web/samples_index/web/images/cookbook/profiling.png
Normal file
|
After Width: | Height: | Size: 718 KiB |
BIN
web/samples_index/web/images/cookbook/reading-writing-files.png
Normal file
|
After Width: | Height: | Size: 656 KiB |
BIN
web/samples_index/web/images/cookbook/retrieve-input.png
Normal file
|
After Width: | Height: | Size: 590 KiB |
BIN
web/samples_index/web/images/cookbook/returning-data.png
Normal file
|
After Width: | Height: | Size: 614 KiB |
BIN
web/samples_index/web/images/cookbook/ripples.png
Normal file
|
After Width: | Height: | Size: 520 KiB |
BIN
web/samples_index/web/images/cookbook/scrolling.png
Normal file
|
After Width: | Height: | Size: 629 KiB |
BIN
web/samples_index/web/images/cookbook/send-data.png
Normal file
|
After Width: | Height: | Size: 594 KiB |
BIN
web/samples_index/web/images/cookbook/snackbars.png
Normal file
|
After Width: | Height: | Size: 585 KiB |
BIN
web/samples_index/web/images/cookbook/sqlite.png
Normal file
|
After Width: | Height: | Size: 629 KiB |
BIN
web/samples_index/web/images/cookbook/tabs.png
Normal file
|
After Width: | Height: | Size: 540 KiB |
BIN
web/samples_index/web/images/cookbook/tap-drag.png
Normal file
|
After Width: | Height: | Size: 621 KiB |
BIN
web/samples_index/web/images/cookbook/text-field-changes.png
Normal file
|
After Width: | Height: | Size: 635 KiB |
BIN
web/samples_index/web/images/cookbook/text-input.png
Normal file
|
After Width: | Height: | Size: 535 KiB |
BIN
web/samples_index/web/images/cookbook/themes.png
Normal file
|
After Width: | Height: | Size: 628 KiB |
BIN
web/samples_index/web/images/cookbook/validation.png
Normal file
|
After Width: | Height: | Size: 638 KiB |
BIN
web/samples_index/web/images/cookbook/web-sockets.png
Normal file
|
After Width: | Height: | Size: 656 KiB |
BIN
web/samples_index/web/images/filipino_cuisine1.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
web/samples_index/web/images/flutter_maps_firestore1.png
Normal file
|
After Width: | Height: | Size: 920 KiB |
BIN
web/samples_index/web/images/flutter_maps_firestore2.png
Normal file
|
After Width: | Height: | Size: 846 KiB |
BIN
web/samples_index/web/images/gallery1.png
Normal file
|
After Width: | Height: | Size: 451 KiB |
BIN
web/samples_index/web/images/gallery2.png
Normal file
|
After Width: | Height: | Size: 301 KiB |
BIN
web/samples_index/web/images/gallery3.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
web/samples_index/web/images/gallery4.png
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
BIN
web/samples_index/web/images/gallery5.png
Normal file
|
After Width: | Height: | Size: 717 KiB |
BIN
web/samples_index/web/images/github_dataviz1.png
Normal file
|
After Width: | Height: | Size: 263 KiB |
BIN
web/samples_index/web/images/isolate1.png
Normal file
|
After Width: | Height: | Size: 139 KiB |