1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-08 13:58:47 +00:00

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>
This commit is contained in:
John Ryan
2020-03-09 16:17:08 -07:00
committed by GitHub
parent efab5b0644
commit 0a5a5109de
183 changed files with 3555 additions and 211 deletions

17
web/samples_index/.gitignore vendored Normal file
View 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

View File

@@ -0,0 +1,3 @@
## 1.0.0
- Initial version, created by Stagehand

View 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`

View 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/**

View 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';

View 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);
}

View 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);
}

File diff suppressed because it is too large Load Diff

View 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);
}

View 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,
};

View 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

View 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();
}

View 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>';
}

View 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;
});

View 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

View 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(''));
});
});
}

View 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

View 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

View 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);
}

View 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));
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 619 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 717 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1012 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Some files were not shown because too many files have changed in this diff Show More