Simplify samples index and remove cookbook recipes (#2102)
Remove the cookbook recipes from the samples index in a step to eventually remove it as a whole. The cookbook recipe listings in the index haven't been updated in a long time, the support for updating them doesn't work, and this isn't generally how people are finding cookbook recipes. This has the added benefit of reducing repo size quite a bit due to the large images.
@@ -1,3 +0,0 @@
|
||||
## 1.0.0
|
||||
|
||||
- Initial version, created by Stagehand
|
||||
@@ -33,12 +33,3 @@ $ dart run grinder 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 docs.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 `dart run grinder scrape-cookbook`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
include: package:lints/recommended.yaml
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
// 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:html/parser.dart' show parse;
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:samples_index/src/data.dart';
|
||||
|
||||
/// Utilities for generating cookbook article data
|
||||
import 'package:webdriver/async_io.dart';
|
||||
|
||||
class CookbookScraper {
|
||||
late WebDriver _driver;
|
||||
|
||||
Future<void> init() async {
|
||||
_driver = await createDriver(desired: <String, dynamic>{});
|
||||
}
|
||||
|
||||
Future<void> 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 search = 'main>.container>header>h1';
|
||||
var h1 = page.querySelector(search);
|
||||
if (h1 == null) {
|
||||
throw ('Could not find match for $search on page $url');
|
||||
}
|
||||
var name = 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,
|
||||
difficulty: 'advanced',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> 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('.');
|
||||
var detailName = p.substring(0, dot);
|
||||
// var categoryName = path.split(link);
|
||||
var components = path.split(link);
|
||||
var categoryName = components[components.length - 2];
|
||||
return '$categoryName-$detailName';
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:checked_yaml/checked_yaml.dart';
|
||||
|
||||
@@ -12,14 +11,10 @@ export 'src/data.dart';
|
||||
|
||||
Future<List<Sample>> getSamples() async {
|
||||
var yamlFile = File('lib/src/samples.yaml');
|
||||
var cookbookFile = File('lib/src/cookbook.json');
|
||||
var contents = await yamlFile.readAsString();
|
||||
var cookbookContents = await cookbookFile.readAsString();
|
||||
var index = checkedYamlDecode(
|
||||
contents, (m) => m != null ? Index.fromJson(m) : null,
|
||||
sourceUrl: yamlFile.uri);
|
||||
if (index == null) throw ('unable to get load from ${yamlFile.uri}');
|
||||
var cookbookIndex =
|
||||
Index.fromJson(json.decode(cookbookContents) as Map<dynamic, dynamic>);
|
||||
return index.samples..addAll(cookbookIndex.samples);
|
||||
return index.samples;
|
||||
}
|
||||
|
||||
@@ -36,11 +36,10 @@ class Sample {
|
||||
/// The author of the sample. Typically "Flutter"
|
||||
final String? author;
|
||||
|
||||
/// Screenshots of the sample or cookbook article. At least 1 screenshot is
|
||||
/// required.
|
||||
/// Screenshots of the sample. At least 1 screenshot is required.
|
||||
final List<Screenshot> screenshots;
|
||||
|
||||
/// A link to the source code or cookbook article if type is 'cookbook'.
|
||||
/// A link to the source code.
|
||||
final String source;
|
||||
|
||||
/// A link to this sample running in the browser.
|
||||
@@ -69,7 +68,7 @@ class Sample {
|
||||
final List<String> platforms;
|
||||
|
||||
/// The type of the sample. Supported values are either 'sample' or
|
||||
/// 'cookbook'.
|
||||
/// 'demo'.
|
||||
final String type;
|
||||
|
||||
/// The date this sample was created.
|
||||
|
||||
@@ -110,12 +110,6 @@ String _indexBody(List<Sample> samples) => '''
|
||||
<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">
|
||||
@@ -205,11 +199,6 @@ String _descriptionButtons(Sample sample) {
|
||||
<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();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
name: samples_index
|
||||
description: A visual index of Flutter samples
|
||||
description: A visual index of Flutter samples.
|
||||
homepage: https://github.com/flutter/samples/tree/main/web/samples_index
|
||||
version: 0.0.1
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ^3.2.0
|
||||
|
||||
dependencies:
|
||||
json_annotation: ^4.8.1
|
||||
path: ^1.8.3
|
||||
yaml: ^3.1.2
|
||||
mdc_web: ^0.6.0
|
||||
sass_builder: ^2.2.1
|
||||
checked_yaml: ^2.0.3
|
||||
webdriver: ^3.0.2
|
||||
html: ^0.15.3
|
||||
json_annotation: ^4.8.1
|
||||
mdc_web: ^0.6.0
|
||||
path: ^1.8.3
|
||||
sass_builder: ^2.2.1
|
||||
yaml: ^3.1.2
|
||||
|
||||
dev_dependencies:
|
||||
grinder: ^0.9.4
|
||||
flutter_lints: ">=2.0.1 <4.0.0"
|
||||
test: ^1.24.2
|
||||
json_serializable: ^6.6.2
|
||||
build: ^2.4.0
|
||||
build_runner: ^2.4.2
|
||||
build_web_compilers: ^4.0.3
|
||||
image: ">=3.2.0 <5.0.0"
|
||||
grinder: ^0.9.4
|
||||
image: ^4.1.3
|
||||
json_serializable: ^6.6.2
|
||||
lints: ^3.0.0
|
||||
test: ^1.24.2
|
||||
|
||||
# package:mdc_web needs to upgrade the version of material-components-web 12.0.0
|
||||
# or above, which includes this fix for the division operator:
|
||||
|
||||
@@ -122,8 +122,8 @@ void main() {
|
||||
|
||||
// Test if queries match by type
|
||||
expect(matchesQuery('type:sample', attributes), true);
|
||||
expect(matchesQuery('type:cookbook', attributes), false);
|
||||
expect(matchesQuery('kittens type:cookbook', attributes), false);
|
||||
expect(matchesQuery('type:demo', attributes), false);
|
||||
expect(matchesQuery('kittens type:demo', attributes), false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -134,7 +134,7 @@ void main() {
|
||||
expect(parseHash('#?search=kittens&platform=web'),
|
||||
containsPair('platform', 'web'));
|
||||
expect(parseHash('#?type=sample'), containsPair('type', 'sample'));
|
||||
expect(parseHash('#?type=cookbook'), containsPair('type', 'cookbook'));
|
||||
expect(parseHash('#?type=demo'), containsPair('type', 'demo'));
|
||||
});
|
||||
|
||||
test('can be set', () {
|
||||
|
||||
@@ -22,6 +22,6 @@ samples:
|
||||
href: https://apps.apple.com/us/app/neko-atsume-kitty-collector/id923917775
|
||||
- text: author
|
||||
href: http://jpryan.me
|
||||
type: sample # sample, app, or cookbook
|
||||
type: sample # sample or app
|
||||
date: 2019-12-15T02:59:43.1Z
|
||||
channel: stable
|
||||
|
||||
@@ -16,6 +16,6 @@ samples:
|
||||
- path
|
||||
tags: ['beginner', 'kittens', 'cats']
|
||||
platforms: ['web', 'ios', 'android']
|
||||
type: sample # sample, app, or cookbook
|
||||
type: sample # sample or app
|
||||
date: 2019-12-15T02:59:43.1Z
|
||||
channel: stable
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:grinder/grinder.dart';
|
||||
import 'package:image/image.dart' as image;
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:samples_index/cookbook.dart';
|
||||
import 'package:samples_index/samples_index.dart';
|
||||
import 'package:samples_index/src/templates.dart' as templates;
|
||||
|
||||
@@ -60,36 +58,9 @@ Future<void> generate() async {
|
||||
log('Generated index for ${samples.length} samples.');
|
||||
}
|
||||
|
||||
@Task('Scrape the cookbook for images and descriptions')
|
||||
Future<void> scrapeCookbook() async {
|
||||
var driver = await Process.start(
|
||||
'chromedriver', ['--port=4444', '--url-base=wd/hub', '--verbose']);
|
||||
await driver.stdout.pipe(stdout);
|
||||
await driver.stderr.pipe(stderr);
|
||||
var scraper = CookbookScraper();
|
||||
await scraper.init();
|
||||
var links = await scraper.fetchCookbookLinks();
|
||||
log('Scraping ${links.length} cookbook articles');
|
||||
var allSamples = <Sample>[];
|
||||
for (final 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 = const JsonEncoder.withIndent('\t');
|
||||
await file.writeAsString(encoder.convert(Index(allSamples)));
|
||||
await scraper.dispose();
|
||||
var killed = driver.kill();
|
||||
if (!killed) {
|
||||
log('failed to kill chromedriver process');
|
||||
}
|
||||
}
|
||||
|
||||
@Task('creates thumbnail images in web/images')
|
||||
Future<void> createThumbnails() async {
|
||||
await _createThumbnails(Directory('web/images'));
|
||||
await _createThumbnails(Directory('web/images/cookbook'));
|
||||
}
|
||||
|
||||
// Creates a thumbnail image for each png file
|
||||
|
||||
|
Before Width: | Height: | Size: 706 KiB |
|
Before Width: | Height: | Size: 498 KiB |
|
Before Width: | Height: | Size: 408 KiB |
|
Before Width: | Height: | Size: 432 KiB |
|
Before Width: | Height: | Size: 429 KiB |
|
Before Width: | Height: | Size: 583 KiB |
|
Before Width: | Height: | Size: 646 KiB |
|
Before Width: | Height: | Size: 462 KiB |
|
Before Width: | Height: | Size: 552 KiB |
|
Before Width: | Height: | Size: 438 KiB |
|
Before Width: | Height: | Size: 445 KiB |
|
Before Width: | Height: | Size: 428 KiB |
|
Before Width: | Height: | Size: 443 KiB |
|
Before Width: | Height: | Size: 435 KiB |
|
Before Width: | Height: | Size: 377 KiB |
|
Before Width: | Height: | Size: 418 KiB |
|
Before Width: | Height: | Size: 584 KiB |
|
Before Width: | Height: | Size: 592 KiB |
|
Before Width: | Height: | Size: 638 KiB |
|
Before Width: | Height: | Size: 555 KiB |
|
Before Width: | Height: | Size: 602 KiB |
|
Before Width: | Height: | Size: 613 KiB |
|
Before Width: | Height: | Size: 731 KiB |
|
Before Width: | Height: | Size: 648 KiB |
|
Before Width: | Height: | Size: 597 KiB |
|
Before Width: | Height: | Size: 468 KiB |
|
Before Width: | Height: | Size: 397 KiB |
|
Before Width: | Height: | Size: 475 KiB |
|
Before Width: | Height: | Size: 389 KiB |
|
Before Width: | Height: | Size: 439 KiB |
|
Before Width: | Height: | Size: 411 KiB |
|
Before Width: | Height: | Size: 389 KiB |
|
Before Width: | Height: | Size: 374 KiB |
|
Before Width: | Height: | Size: 538 KiB |
|
Before Width: | Height: | Size: 550 KiB |
|
Before Width: | Height: | Size: 608 KiB |
|
Before Width: | Height: | Size: 461 KiB |
|
Before Width: | Height: | Size: 404 KiB |
|
Before Width: | Height: | Size: 406 KiB |
|
Before Width: | Height: | Size: 372 KiB |
|
Before Width: | Height: | Size: 444 KiB |
|
Before Width: | Height: | Size: 479 KiB |
|
Before Width: | Height: | Size: 436 KiB |
|
Before Width: | Height: | Size: 688 KiB |
|
Before Width: | Height: | Size: 582 KiB |
|
Before Width: | Height: | Size: 341 KiB |
|
Before Width: | Height: | Size: 471 KiB |
|
Before Width: | Height: | Size: 405 KiB |
|
Before Width: | Height: | Size: 349 KiB |
|
Before Width: | Height: | Size: 401 KiB |
|
Before Width: | Height: | Size: 402 KiB |
|
Before Width: | Height: | Size: 555 KiB |
|
Before Width: | Height: | Size: 496 KiB |
|
Before Width: | Height: | Size: 600 KiB |
|
Before Width: | Height: | Size: 644 KiB |
|
Before Width: | Height: | Size: 619 KiB |
|
Before Width: | Height: | Size: 669 KiB |
|
Before Width: | Height: | Size: 612 KiB |
|
Before Width: | Height: | Size: 416 KiB |
|
Before Width: | Height: | Size: 436 KiB |
|
Before Width: | Height: | Size: 480 KiB |
|
Before Width: | Height: | Size: 432 KiB |
|
Before Width: | Height: | Size: 497 KiB |
|
Before Width: | Height: | Size: 457 KiB |
|
Before Width: | Height: | Size: 532 KiB |
|
Before Width: | Height: | Size: 392 KiB |
|
Before Width: | Height: | Size: 470 KiB |
|
Before Width: | Height: | Size: 372 KiB |
|
Before Width: | Height: | Size: 444 KiB |
|
Before Width: | Height: | Size: 425 KiB |
|
Before Width: | Height: | Size: 449 KiB |
|
Before Width: | Height: | Size: 464 KiB |
|
Before Width: | Height: | Size: 598 KiB |
|
Before Width: | Height: | Size: 602 KiB |
|
Before Width: | Height: | Size: 590 KiB |
|
Before Width: | Height: | Size: 626 KiB |
|
Before Width: | Height: | Size: 644 KiB |
|
Before Width: | Height: | Size: 424 KiB |
|
Before Width: | Height: | Size: 415 KiB |
|
Before Width: | Height: | Size: 479 KiB |
|
Before Width: | Height: | Size: 590 KiB |
|
Before Width: | Height: | Size: 604 KiB |
|
Before Width: | Height: | Size: 616 KiB |
|
Before Width: | Height: | Size: 474 KiB |
|
Before Width: | Height: | Size: 460 KiB |
|
Before Width: | Height: | Size: 718 KiB |
|
Before Width: | Height: | Size: 656 KiB |