Remove visual samples index (#2607)
Removes VSI and all related infra. It has been replaced by [an index page on flutter.dev](https://docs.flutter.dev/reference/learning-resources) Fixes: #2582 #2507 ## Pre-launch Checklist - [x] I read the [Flutter Style Guide] _recently_, and have followed its advice. - [x] I signed the [CLA]. - [x] I read the [Contributors Guide]. - [x] I have added sample code updates to the [changelog]. - [x] I updated/added relevant documentation (doc comments with `///`).pull/2617/head
@ -1,38 +0,0 @@
|
||||
name: Deploy to GitHub Pages
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions: read-all
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'flutter/samples'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
submodules: true
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046
|
||||
|
||||
- name: Init scripts
|
||||
run: dart pub get
|
||||
working-directory: web/_tool
|
||||
|
||||
- name: Build
|
||||
run: dart run _tool/build_ci.dart
|
||||
working-directory: web
|
||||
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: web/samples_index/public
|
@ -1,36 +0,0 @@
|
||||
name: Verify web demos
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions: read-all
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
verify-web-demos:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'flutter/samples'
|
||||
strategy:
|
||||
matrix:
|
||||
flutter_version:
|
||||
- stable
|
||||
# TODO(johnpryan): https://github.com/flutter/samples/issues/1469
|
||||
# - beta
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
submodules: true
|
||||
fetch-depth: 0
|
||||
- uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046
|
||||
with:
|
||||
channel: ${{ matrix.flutter_version }}
|
||||
- name: Init scripts
|
||||
run: dart pub get
|
||||
working-directory: web/_tool
|
||||
- name: Verify packages
|
||||
run: dart run _tool/verify_packages.dart
|
||||
working-directory: web
|
@ -1 +0,0 @@
|
||||
*/build
|
@ -1,29 +0,0 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
build/
|
@ -1,10 +0,0 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "f5fb61b953a631f47191124a31169701911ee1f4"
|
||||
channel: "main"
|
||||
|
||||
project_type: package
|
@ -1,4 +0,0 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
@ -1,43 +0,0 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
@ -1,45 +0,0 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "f5fb61b953a631f47191124a31169701911ee1f4"
|
||||
channel: "main"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: f5fb61b953a631f47191124a31169701911ee1f4
|
||||
base_revision: f5fb61b953a631f47191124a31169701911ee1f4
|
||||
- platform: android
|
||||
create_revision: f5fb61b953a631f47191124a31169701911ee1f4
|
||||
base_revision: f5fb61b953a631f47191124a31169701911ee1f4
|
||||
- platform: ios
|
||||
create_revision: f5fb61b953a631f47191124a31169701911ee1f4
|
||||
base_revision: f5fb61b953a631f47191124a31169701911ee1f4
|
||||
- platform: linux
|
||||
create_revision: f5fb61b953a631f47191124a31169701911ee1f4
|
||||
base_revision: f5fb61b953a631f47191124a31169701911ee1f4
|
||||
- platform: macos
|
||||
create_revision: f5fb61b953a631f47191124a31169701911ee1f4
|
||||
base_revision: f5fb61b953a631f47191124a31169701911ee1f4
|
||||
- platform: web
|
||||
create_revision: f5fb61b953a631f47191124a31169701911ee1f4
|
||||
base_revision: f5fb61b953a631f47191124a31169701911ee1f4
|
||||
- platform: windows
|
||||
create_revision: f5fb61b953a631f47191124a31169701911ee1f4
|
||||
base_revision: f5fb61b953a631f47191124a31169701911ee1f4
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
@ -1,3 +0,0 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
linter:
|
||||
rules:
|
@ -1,147 +0,0 @@
|
||||
// Copyright 2021 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.
|
||||
|
||||
// ignore_for_file: avoid_print
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:web_startup_analyzer/web_startup_analyzer.dart';
|
||||
|
||||
main() async {
|
||||
var analyzer = WebStartupAnalyzer(additionalFrameCount: 10);
|
||||
print(json.encode(analyzer.startupTiming));
|
||||
analyzer.onFirstFrame.addListener(() {
|
||||
print(json.encode({'firstFrame': analyzer.onFirstFrame.value}));
|
||||
});
|
||||
analyzer.onFirstPaint.addListener(() {
|
||||
print(
|
||||
json.encode({
|
||||
'firstPaint': analyzer.onFirstPaint.value?.$1,
|
||||
'firstContentfulPaint': analyzer.onFirstPaint.value?.$2,
|
||||
}),
|
||||
);
|
||||
});
|
||||
analyzer.onAdditionalFrames.addListener(() {
|
||||
print(json.encode({'additionalFrames': analyzer.onAdditionalFrames.value}));
|
||||
});
|
||||
runApp(WebStartupAnalyzerSample(analyzer: analyzer));
|
||||
}
|
||||
|
||||
class WebStartupAnalyzerSample extends StatelessWidget {
|
||||
final WebStartupAnalyzer analyzer;
|
||||
const WebStartupAnalyzerSample({super.key, required this.analyzer});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Flutter web app timing',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green.shade100),
|
||||
),
|
||||
home: WebStartupAnalyzerScreen(analyzer: analyzer),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WebStartupAnalyzerScreen extends StatefulWidget {
|
||||
final WebStartupAnalyzer analyzer;
|
||||
|
||||
const WebStartupAnalyzerScreen({super.key, required this.analyzer});
|
||||
|
||||
@override
|
||||
State<WebStartupAnalyzerScreen> createState() =>
|
||||
_WebStartupAnalyzerScreenState();
|
||||
}
|
||||
|
||||
class _WebStartupAnalyzerScreenState extends State<WebStartupAnalyzerScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.amber.shade50,
|
||||
body: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(8.0),
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: ListenableBuilder(
|
||||
listenable: widget.analyzer.onChange,
|
||||
builder: (BuildContext context, child) {
|
||||
return ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
TimingWidget(
|
||||
name: 'DCL',
|
||||
timingMs: widget.analyzer.domContentLoaded,
|
||||
),
|
||||
TimingWidget(
|
||||
name: 'Load entrypoint',
|
||||
timingMs: widget.analyzer.loadEntrypoint,
|
||||
),
|
||||
TimingWidget(
|
||||
name: 'Initialize engine',
|
||||
timingMs: widget.analyzer.initializeEngine,
|
||||
),
|
||||
TimingWidget(
|
||||
name: 'Run app',
|
||||
timingMs: widget.analyzer.appRunnerRunApp,
|
||||
),
|
||||
if (widget.analyzer.firstFrame != null)
|
||||
TimingWidget(
|
||||
name: 'First frame',
|
||||
timingMs: widget.analyzer.firstFrame!,
|
||||
),
|
||||
if (widget.analyzer.firstPaint != null)
|
||||
TimingWidget(
|
||||
name: 'First paint',
|
||||
timingMs: widget.analyzer.firstPaint!,
|
||||
),
|
||||
if (widget.analyzer.firstContentfulPaint != null)
|
||||
TimingWidget(
|
||||
name: 'First contentful paint',
|
||||
timingMs: widget.analyzer.firstContentfulPaint!,
|
||||
),
|
||||
if (widget.analyzer.additionalFrames != null) ...[
|
||||
for (var i in widget.analyzer.additionalFrames!)
|
||||
TimingWidget(name: 'Frame', timingMs: i.toDouble()),
|
||||
] else
|
||||
TextButton(
|
||||
child: const Text('Trigger frames'),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TimingWidget extends StatelessWidget {
|
||||
final String name;
|
||||
final double timingMs;
|
||||
|
||||
const TimingWidget({super.key, required this.name, required this.timingMs});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
name,
|
||||
style: const TextStyle(fontSize: 18),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: Text(
|
||||
'${timingMs.truncate()}ms',
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
name: example
|
||||
description: "flutter_web_startup_analyzer example"
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
version: 1.0.0+1
|
||||
environment:
|
||||
sdk: ^3.7.0-0
|
||||
flutter: ^3.16.0
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
cupertino_icons: ^1.0.6
|
||||
web_startup_analyzer:
|
||||
path: ../
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^5.0.0
|
||||
flutter:
|
||||
uses-material-design: true
|
Before Width: | Height: | Size: 917 B |
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 20 KiB |
@ -1,50 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<base href="$FLUTTER_BASE_HREF">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="web_startup_analyzer example">
|
||||
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="example">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<title>web_perf_metrics example</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
|
||||
<script>
|
||||
const serviceWorkerVersion = null;
|
||||
</script>
|
||||
<script src="flutter.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="text/javascript" src="assets/packages/web_startup_analyzer/lib/web_startup_analyzer.js"></script>
|
||||
<script>
|
||||
var flutterWebStartupAnalyzer = new FlutterWebStartupAnalyzer();
|
||||
var analyzer = flutterWebStartupAnalyzer;
|
||||
|
||||
window.addEventListener('load', function(ev) {
|
||||
analyzer.markStart("loadEntrypoint");
|
||||
_flutter.loader.loadEntrypoint({
|
||||
serviceWorker: {
|
||||
serviceWorkerVersion: serviceWorkerVersion,
|
||||
},
|
||||
onEntrypointLoaded: function(engineInitializer) {
|
||||
analyzer.markFinished("loadEntrypoint");
|
||||
analyzer.markStart("initializeEngine");
|
||||
engineInitializer.initializeEngine().then(function(appRunner) {
|
||||
analyzer.markFinished("initializeEngine");
|
||||
analyzer.markStart("appRunnerRunApp");
|
||||
appRunner.runApp();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "example",
|
||||
"short_name": "example",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#0175C2",
|
||||
"theme_color": "#0175C2",
|
||||
"description": "A new Flutter project.",
|
||||
"orientation": "portrait-primary",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/Icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
// Copyright 2021 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:async';
|
||||
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class FrameAnalyzer {
|
||||
final WidgetsBinding _binding;
|
||||
final Completer _onDone = Completer();
|
||||
int _remainingFrames;
|
||||
|
||||
final int additionalFrames;
|
||||
List<int> additionalFrameTimes = [];
|
||||
|
||||
FrameAnalyzer(this._binding, {this.additionalFrames = 10})
|
||||
: _remainingFrames = additionalFrames;
|
||||
|
||||
Future<void> captureAdditionalFrames() {
|
||||
_binding.addTimingsCallback(_timingsCallback);
|
||||
return _onDone.future;
|
||||
}
|
||||
|
||||
void _reportFrame(FrameTiming frameTiming) {
|
||||
additionalFrameTimes.add(frameTiming.totalSpan.inMilliseconds);
|
||||
}
|
||||
|
||||
void _timingsCallback(List<FrameTiming> timings) {
|
||||
int i = 0;
|
||||
while (_remainingFrames > 0 && i < timings.length) {
|
||||
_reportFrame(timings[i]);
|
||||
i++;
|
||||
_remainingFrames--;
|
||||
}
|
||||
if (_remainingFrames <= 0) {
|
||||
_binding.removeTimingsCallback(_timingsCallback);
|
||||
|
||||
_onDone.complete();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
// Copyright 2021 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:js_interop';
|
||||
|
||||
@JS()
|
||||
@staticInterop
|
||||
external FlutterWebStartupAnalyzer get flutterWebStartupAnalyzer;
|
||||
|
||||
@JS()
|
||||
@staticInterop
|
||||
class FlutterWebStartupAnalyzer {
|
||||
external factory FlutterWebStartupAnalyzer();
|
||||
}
|
||||
|
||||
extension FlutterWebStartupAnalyzerExtensions on FlutterWebStartupAnalyzer {
|
||||
external JSObject get timings;
|
||||
external void markStart(String name);
|
||||
external void markFinished(String name);
|
||||
external void capture(String name);
|
||||
external void captureAll();
|
||||
external void capturePaint();
|
||||
external void report();
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
// Copyright 2021 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:async';
|
||||
import 'dart:js_interop';
|
||||
import 'dart:js_interop_unsafe';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:web_startup_analyzer/src/web_startup_analyzer_base.dart';
|
||||
|
||||
import 'frame_analyzer.dart';
|
||||
import 'startup_analyzer.dart';
|
||||
|
||||
class WebStartupAnalyzer extends WebStartupAnalyzerBase {
|
||||
final WidgetsBinding _widgetsBinding;
|
||||
late final FrameAnalyzer _frameAnalyzer;
|
||||
List<int>? _additionalFrames;
|
||||
|
||||
@override
|
||||
Map<String, dynamic> startupTiming = {};
|
||||
|
||||
@override
|
||||
double get domContentLoaded =>
|
||||
(flutterWebStartupAnalyzer.timings['domContentLoaded'] as JSNumber)
|
||||
.toDartDouble;
|
||||
@override
|
||||
double get loadEntrypoint =>
|
||||
(flutterWebStartupAnalyzer.timings['loadEntrypoint'] as JSNumber)
|
||||
.toDartDouble;
|
||||
@override
|
||||
double get initializeEngine =>
|
||||
(flutterWebStartupAnalyzer.timings['initializeEngine'] as JSNumber)
|
||||
.toDartDouble;
|
||||
@override
|
||||
double get appRunnerRunApp =>
|
||||
(flutterWebStartupAnalyzer.timings['appRunnerRunApp'] as JSNumber)
|
||||
.toDartDouble;
|
||||
@override
|
||||
double? get firstFrame =>
|
||||
(flutterWebStartupAnalyzer.timings['firstFrame'] as JSNumber?)
|
||||
?.toDartDouble;
|
||||
@override
|
||||
double? get firstPaint =>
|
||||
(flutterWebStartupAnalyzer.timings['first-paint'] as JSNumber?)
|
||||
?.toDartDouble;
|
||||
@override
|
||||
double? get firstContentfulPaint =>
|
||||
(flutterWebStartupAnalyzer.timings['first-contentful-paint'] as JSNumber?)
|
||||
?.toDartDouble;
|
||||
@override
|
||||
List<int>? get additionalFrames => _additionalFrames;
|
||||
|
||||
WebStartupAnalyzer({int additionalFrameCount = 5})
|
||||
: _widgetsBinding = WidgetsFlutterBinding.ensureInitialized() {
|
||||
_frameAnalyzer = FrameAnalyzer(
|
||||
_widgetsBinding,
|
||||
additionalFrames: additionalFrameCount,
|
||||
);
|
||||
_captureStartupMetrics();
|
||||
startupTiming = {
|
||||
'domContentLoaded': domContentLoaded,
|
||||
'loadEntrypoint': loadEntrypoint,
|
||||
'initializeEngine': initializeEngine,
|
||||
'appRunnerRunApp': appRunnerRunApp,
|
||||
};
|
||||
_captureFirstFrame().then((value) {
|
||||
flutterWebStartupAnalyzer.captureAll();
|
||||
onFirstFrame.value = firstFrame;
|
||||
|
||||
// Capture first-paint and first-contentful-paint
|
||||
Future.delayed(const Duration(milliseconds: 200)).then((_) {
|
||||
flutterWebStartupAnalyzer.capturePaint();
|
||||
onFirstPaint.value = (firstPaint!, firstContentfulPaint!);
|
||||
});
|
||||
});
|
||||
captureFlutterFrameData().then((value) {
|
||||
_additionalFrames = value;
|
||||
onAdditionalFrames.value = value;
|
||||
});
|
||||
onChange = Listenable.merge([
|
||||
onFirstFrame,
|
||||
onFirstPaint,
|
||||
onAdditionalFrames,
|
||||
]);
|
||||
}
|
||||
|
||||
_captureStartupMetrics() {
|
||||
flutterWebStartupAnalyzer.markFinished('appRunnerRunApp');
|
||||
flutterWebStartupAnalyzer.captureAll();
|
||||
}
|
||||
|
||||
Future<void> _captureFirstFrame() {
|
||||
final completer = Completer();
|
||||
flutterWebStartupAnalyzer.markStart('firstFrame');
|
||||
_widgetsBinding.addPostFrameCallback((timeStamp) {
|
||||
flutterWebStartupAnalyzer.markFinished('firstFrame');
|
||||
flutterWebStartupAnalyzer.capture('firstFrame');
|
||||
completer.complete();
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<List<int>> captureFlutterFrameData() async {
|
||||
await _frameAnalyzer.captureAdditionalFrames();
|
||||
return _frameAnalyzer.additionalFrameTimes;
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
// Copyright 2021 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 'package:flutter/foundation.dart';
|
||||
|
||||
// Base class for the web (real) implementation and dart:io (stub)
|
||||
// implementation.
|
||||
abstract class WebStartupAnalyzerBase {
|
||||
late final Listenable onChange;
|
||||
ValueNotifier<double?> onFirstFrame = ValueNotifier(null);
|
||||
ValueNotifier<(double, double)?> onFirstPaint = ValueNotifier(null);
|
||||
ValueNotifier<List<int>?> onAdditionalFrames = ValueNotifier(null);
|
||||
|
||||
double get domContentLoaded;
|
||||
double get loadEntrypoint;
|
||||
double get initializeEngine;
|
||||
double get appRunnerRunApp;
|
||||
double? get firstFrame;
|
||||
double? get firstPaint;
|
||||
double? get firstContentfulPaint;
|
||||
List<int>? get additionalFrames;
|
||||
|
||||
Map<String, dynamic> get startupTiming;
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
// Copyright 2021 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 'web_startup_analyzer_base.dart';
|
||||
|
||||
// This class is a stub so that unit tests can run without importing
|
||||
// dart:js_interop and related packages.
|
||||
class WebStartupAnalyzer extends WebStartupAnalyzerBase {
|
||||
WebStartupAnalyzer({int additionalFrameCount = 0});
|
||||
|
||||
@override
|
||||
List<int>? get additionalFrames => [];
|
||||
|
||||
@override
|
||||
double get appRunnerRunApp => 0.0;
|
||||
|
||||
@override
|
||||
double get domContentLoaded => 0.0;
|
||||
|
||||
@override
|
||||
double? get firstContentfulPaint => 0.0;
|
||||
|
||||
@override
|
||||
double? get firstFrame => 0.0;
|
||||
|
||||
@override
|
||||
double? get firstPaint => 0.0;
|
||||
|
||||
@override
|
||||
double get initializeEngine => 0.0;
|
||||
|
||||
@override
|
||||
double get loadEntrypoint => 0.0;
|
||||
|
||||
@override
|
||||
Map<String, dynamic> get startupTiming => {};
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
// Copyright 2021 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/web_startup_analyzer_io.dart'
|
||||
if (dart.library.js_interop) 'src/web_startup_analyzer.dart';
|
@ -1,47 +0,0 @@
|
||||
// Copyright 2021 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.
|
||||
|
||||
// Helper class to capture Flutter web app startup timing information
|
||||
class FlutterWebStartupAnalyzer {
|
||||
timings;
|
||||
|
||||
constructor() {
|
||||
this.timings = {};
|
||||
}
|
||||
|
||||
markStart(name) {
|
||||
this.timings[name] = null;
|
||||
performance.mark('flt-' + name + '-started');
|
||||
}
|
||||
markFinished(name) {
|
||||
performance.mark('flt-' + name + '-finished');
|
||||
}
|
||||
capture(name) {
|
||||
var timingName = 'flt-' + name;
|
||||
var started = 'flt-' + name + 'started';
|
||||
try {
|
||||
var measurement = performance.measure('flt-' + name, 'flt-' + name + '-started', 'flt-' + name + '-finished');
|
||||
} catch(e) {
|
||||
// ignore errors if the mark doesn't exist
|
||||
return;
|
||||
}
|
||||
this.timings[name] = measurement.duration;
|
||||
}
|
||||
captureAll() {
|
||||
for (var [key, value] of Object.entries(this.timings)) {
|
||||
this.capture(key);
|
||||
}
|
||||
// Capture
|
||||
this.timings['load'] = performance.timing.loadEventEnd - performance.timing.domContentLoadedEventEnd;
|
||||
this.timings['domContentLoaded'] = performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart;
|
||||
}
|
||||
|
||||
capturePaint() {
|
||||
const entries = performance.getEntriesByType("paint");
|
||||
// Collect first-paint and first-contentful-paint entries
|
||||
entries.forEach((entry) => {
|
||||
this.timings[entry.name] = entry.startTime;
|
||||
});
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
name: web_startup_analyzer
|
||||
description: "Captures web startup timing data in a Flutter web app"
|
||||
version: 0.1.0-wip
|
||||
|
||||
environment:
|
||||
sdk: ^3.7.0-0
|
||||
flutter: ^3.16.0
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^5.0.0
|
||||
flutter:
|
||||
assets:
|
||||
- lib/web_startup_analyzer.js
|
@ -1,3 +0,0 @@
|
||||
// Copyright {{ year }} 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
|
@ -1 +0,0 @@
|
||||
include: package:lints/recommended.yaml
|
@ -1,76 +0,0 @@
|
||||
// Copyright 2021 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:path/path.dart' as p;
|
||||
|
||||
import 'common.dart';
|
||||
import 'fix_base_tags.dart';
|
||||
|
||||
const ignoredDirectories = [
|
||||
'_tool',
|
||||
'_packages/web_startup_analyzer',
|
||||
'_packages/web_startup_analyzer/example',
|
||||
'samples_index',
|
||||
];
|
||||
|
||||
void main() async {
|
||||
final packageDirs = [
|
||||
...listPackageDirs(Directory.current)
|
||||
.map((path) => p.relative(path, from: Directory.current.path))
|
||||
.where((path) => !ignoredDirectories.contains(path)),
|
||||
];
|
||||
|
||||
print('Building the sample index...');
|
||||
await _run('samples_index', 'flutter', ['pub', 'get']);
|
||||
await _run('samples_index', 'flutter', ['pub', 'run', 'grinder', 'deploy']);
|
||||
|
||||
// Create the directory each Flutter Web sample lives in
|
||||
Directory(
|
||||
p.join(Directory.current.path, 'samples_index', 'public', 'web'),
|
||||
).createSync(recursive: true);
|
||||
|
||||
for (var i = 0; i < packageDirs.length; i++) {
|
||||
var directory = packageDirs[i];
|
||||
|
||||
logWrapped(ansiMagenta, '\n$directory (${i + 1} of ${packageDirs.length})');
|
||||
|
||||
// Create the target directory
|
||||
var directoryName = p.basename(directory);
|
||||
var sourceBuildDir = p.join(
|
||||
Directory.current.path,
|
||||
directory,
|
||||
'build',
|
||||
'web',
|
||||
);
|
||||
var targetDirectory = p.join(
|
||||
Directory.current.path,
|
||||
'samples_index',
|
||||
'public',
|
||||
'web',
|
||||
directoryName,
|
||||
);
|
||||
|
||||
// Build the sample and copy the files
|
||||
await _run(directory, 'flutter', ['pub', 'get']);
|
||||
await _run(directory, 'flutter', ['build', 'web', '--wasm']);
|
||||
await _run(directory, 'mv', [sourceBuildDir, targetDirectory]);
|
||||
}
|
||||
|
||||
// Update the <base href> tags in each index.html file
|
||||
await fixBaseTags();
|
||||
}
|
||||
|
||||
// Invokes run() and exits if the sub-process failed.
|
||||
Future<void> _run(
|
||||
String workingDir,
|
||||
String commandName,
|
||||
List<String> args,
|
||||
) async {
|
||||
var success = await run(workingDir, commandName, args);
|
||||
if (!success) {
|
||||
exit(1);
|
||||
}
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
Future<void> main() async {
|
||||
await fixBaseTags();
|
||||
}
|
||||
|
||||
/// Changes each sample's `<base href="/">` tag in index.html to
|
||||
/// `<base href="/samples/web/<SAMPLE_DIR_NAME>/">`
|
||||
///
|
||||
/// For example, after building using `build_ci.dart,
|
||||
/// `../samples_index/public/web/navigation_and_routing/index.html` should
|
||||
/// contain `<base href="/samples/web/navigation_and_routing/">`
|
||||
Future<void> fixBaseTags() async {
|
||||
print('currentDir = ${Directory.current.path}');
|
||||
var builtSamplesDir = Directory(
|
||||
p.joinAll([
|
||||
// Parent directory
|
||||
...p.split(Directory.current.path),
|
||||
// path to built samples
|
||||
...p.split('samples_index/public/web'),
|
||||
]),
|
||||
);
|
||||
if (!await builtSamplesDir.exists()) {
|
||||
print('${builtSamplesDir.path} does not exist.');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
await for (var builtSample in builtSamplesDir.list()) {
|
||||
if (builtSample is Directory) {
|
||||
var index = File(p.join(builtSample.path, 'index.html'));
|
||||
if (!await index.exists()) {
|
||||
throw ('no index.html file found in ${builtSample.path}');
|
||||
}
|
||||
|
||||
var sampleDirName = p.split(builtSample.path).last;
|
||||
|
||||
if (await index.exists()) {
|
||||
final regex = RegExp('<base href="(.*)">');
|
||||
var contents = await index.readAsString();
|
||||
if (!contents.contains(regex)) {
|
||||
continue;
|
||||
}
|
||||
var newContents = contents.replaceFirst(
|
||||
regex,
|
||||
'<base href="/samples/web/$sampleDirName/">',
|
||||
);
|
||||
await index.writeAsString(newContents);
|
||||
}
|
||||
|
||||
// Since all of the web examples use the hosted canvaskit bits
|
||||
// There is no need to deploy these
|
||||
final canvasKitDirectory = Directory(
|
||||
p.join(builtSample.path, 'canvaskit'),
|
||||
);
|
||||
if (canvasKitDirectory.existsSync()) {
|
||||
canvasKitDirectory.deleteSync(recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
name: tool
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ^3.7.0-0
|
||||
|
||||
dependencies:
|
||||
markdown: ^7.0.0
|
||||
path: ^1.8.0
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^5.0.0
|
@ -1,63 +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:path/path.dart' as p;
|
||||
|
||||
import 'common.dart';
|
||||
|
||||
void main() async {
|
||||
final packageDirs =
|
||||
listPackageDirs(Directory.current)
|
||||
.map((path) => p.relative(path, from: Directory.current.path))
|
||||
.where((path) => !p.dirname(path).startsWith('_'))
|
||||
.toList();
|
||||
|
||||
print('Package dirs:\n${packageDirs.map((path) => ' $path').join('\n')}');
|
||||
|
||||
final results = <bool>[];
|
||||
for (var i = 0; i < packageDirs.length; i++) {
|
||||
final dir = packageDirs[i];
|
||||
logWrapped(ansiMagenta, '\n$dir (${i + 1} of ${packageDirs.length})');
|
||||
|
||||
final upgradeResult = await run(dir, 'flutter', [
|
||||
'pub',
|
||||
'pub',
|
||||
'upgrade',
|
||||
'--no-precompile',
|
||||
]);
|
||||
|
||||
results.add(upgradeResult);
|
||||
if (!upgradeResult) {
|
||||
// skipping analyze when `pub upgrade` fails.
|
||||
results.add(false);
|
||||
continue;
|
||||
}
|
||||
results.add(
|
||||
await run(dir, 'dart', [
|
||||
'analyze',
|
||||
'--fatal-infos',
|
||||
'--fatal-warnings',
|
||||
'.',
|
||||
]),
|
||||
);
|
||||
_printStatus(results);
|
||||
}
|
||||
|
||||
if (results.any((v) => !v)) {
|
||||
exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void _printStatus(List<bool> results) {
|
||||
var successCount = results.where((t) => t).length;
|
||||
var success = (successCount == results.length);
|
||||
var pct = 100 * successCount / results.length;
|
||||
|
||||
logWrapped(
|
||||
success ? ansiGreen : ansiRed,
|
||||
'$successCount of ${results.length} (${pct.toStringAsFixed(2)}%)',
|
||||
);
|
||||
}
|
@ -1 +0,0 @@
|
||||
../animations
|
@ -1 +0,0 @@
|
||||
../form_app
|
@ -1 +0,0 @@
|
||||
../game_template
|
@ -1 +0,0 @@
|
||||
../material_3_demo
|
@ -1 +0,0 @@
|
||||
../navigation_and_routing
|
@ -1,15 +0,0 @@
|
||||
# Configuration for https://pub.dartlang.org/packages/peanut
|
||||
|
||||
directories:
|
||||
- animations/web
|
||||
- provider_shopper/web
|
||||
- charts/web
|
||||
- filipino_cuisine/web
|
||||
- github_dataviz/web
|
||||
- particle_background/web
|
||||
- slide_puzzle/web
|
||||
- form_app/web
|
||||
- web_dashboard/web
|
||||
- place_tracker/web
|
||||
|
||||
post-build-dart-script: _tool/peanut_post_build.dart
|
@ -1 +0,0 @@
|
||||
../place_tracker
|
@ -1 +0,0 @@
|
||||
../provider_shopper
|
@ -1,57 +0,0 @@
|
||||
# Sample Index and Web Demos
|
||||
|
||||
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
|
||||
https://flutter.github.io/samples/#?platform=web.
|
||||
|
||||
## Building samples code
|
||||
|
||||
Run the demo using the `chrome` device type:
|
||||
|
||||
```console
|
||||
$ cd charts
|
||||
$ flutter packages get
|
||||
$ flutter run -d chrome
|
||||
```
|
||||
|
||||
You should see a message printing the URL to access: `http://localhost:8080`
|
||||
|
||||
## Deploying to GitHub Pages
|
||||
|
||||
This project uses a GitHub action to deploy update the `gh-pages` branch. To
|
||||
do this manually, you can also use `package:peanut`:
|
||||
|
||||
```console
|
||||
$ flutter pub global activate peanut
|
||||
```
|
||||
|
||||
Verify `pub get` has been run on each demo:
|
||||
|
||||
```console
|
||||
$ dart run _tool/verify_packages.dart
|
||||
```
|
||||
|
||||
Build all demos, along with the sample index:
|
||||
|
||||
```console
|
||||
$ flutter pub global run peanut
|
||||
```
|
||||
|
||||
Deploy to GitHub Pages:
|
||||
|
||||
```console
|
||||
$ git push origin gh-pages:gh-pages
|
||||
```
|
||||
|
||||
## Building the sample index
|
||||
|
||||
See sample_index/README.md for details
|
||||
|
||||
[web]: https://flutter.dev/web
|
||||
[samples]: https://flutter.github.io/samples/
|
||||
[peanut]: https://github.com/kevmoo/peanut.dart
|
||||
|
@ -1,19 +0,0 @@
|
||||
# firebase public directory
|
||||
public/
|
||||
|
||||
# Files and directories created by pub
|
||||
.dart_tool/
|
||||
# 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
|
||||
|
||||
# Any thumbnails should be ignored
|
||||
web/images/**/*_thumb.png
|
@ -1,35 +0,0 @@
|
||||
# Flutter samples index generator
|
||||
|
||||
This tool is used to generate the visual
|
||||
[samples index for Flutter samples](https://flutter.github.io/samples/).
|
||||
|
||||
## Generating the index
|
||||
|
||||
We use [grinder](https://pub.dev/packages/grinder) to run the build tasks:
|
||||
|
||||
```bash
|
||||
$ dart pub get
|
||||
$ dart run grinder generate
|
||||
```
|
||||
|
||||
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
|
||||
$ dart pub global activate grinder
|
||||
$ webdev serve
|
||||
```
|
||||
|
||||
## Publishing the index
|
||||
|
||||
You can build the complete index into a publishable directory using Grinder:
|
||||
|
||||
```bash
|
||||
$ dart run grinder build-release
|
||||
```
|
||||
|
||||
This outputs the completely built index to `./public`.
|
@ -1,26 +0,0 @@
|
||||
include: package:lints/recommended.yaml
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
- lib/src/data.g.dart
|
||||
language:
|
||||
strict-casts: true
|
||||
strict-inference: true
|
||||
strict-raw-types: true
|
||||
|
||||
linter:
|
||||
rules:
|
||||
avoid_types_on_closure_parameters: true
|
||||
avoid_void_async: true
|
||||
cancel_subscriptions: true
|
||||
close_sinks: true
|
||||
directives_ordering: true
|
||||
package_prefixed_library_names: true
|
||||
prefer_final_in_for_each: true
|
||||
prefer_single_quotes: true
|
||||
test_types_in_equals: true
|
||||
throw_in_finally: true
|
||||
unawaited_futures: true
|
||||
unnecessary_statements: true
|
||||
use_enums: true
|
||||
use_super_parameters: true
|
@ -1,5 +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
|
||||
|
||||
export 'src/search.dart';
|
@ -1,20 +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:checked_yaml/checked_yaml.dart';
|
||||
|
||||
import 'src/data.dart';
|
||||
|
||||
export 'src/data.dart';
|
||||
|
||||
Future<List<Sample>> getSamples() async {
|
||||
var yamlFile = File('lib/src/samples.yaml');
|
||||
var contents = await yamlFile.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}');
|
||||
return index.samples;
|
||||
}
|
@ -1,250 +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
|
||||
|
||||
// TODO(kevmoo): https://github.com/flutter/samples/issues/2582
|
||||
// ignore: deprecated_member_use
|
||||
import 'dart:html';
|
||||
|
||||
class Carousel {
|
||||
final bool withArrowKeyControl;
|
||||
|
||||
final Element container = querySelector('.slider-container')!;
|
||||
final List<Element> slides = querySelectorAll('.slider-single');
|
||||
|
||||
late int currentSlideIndex;
|
||||
late final int lastSlideIndex;
|
||||
|
||||
late Element prevSlide, currentSlide, nextSlide;
|
||||
|
||||
late num x0;
|
||||
bool touched = false;
|
||||
|
||||
Carousel.init({this.withArrowKeyControl = false}) {
|
||||
lastSlideIndex = slides.length - 1;
|
||||
currentSlideIndex = -1;
|
||||
|
||||
// Remove container empty space when no images are available
|
||||
if (lastSlideIndex == -1) {
|
||||
container.classes.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip carousel decoration when only one image is available
|
||||
if (lastSlideIndex == 0) {
|
||||
currentSlide = slides[currentSlideIndex + 1];
|
||||
currentSlide.classes.add('active');
|
||||
return;
|
||||
}
|
||||
|
||||
_hideSlides();
|
||||
_initBullets();
|
||||
_initArrows();
|
||||
_initGestureListener();
|
||||
|
||||
if (withArrowKeyControl) {
|
||||
_initArrowKeyControl();
|
||||
}
|
||||
|
||||
// Move to the first slide after init
|
||||
// This is responsible for creating a smooth animation
|
||||
Future<void>.delayed(const Duration(milliseconds: 500))
|
||||
.then((value) => _slideRight());
|
||||
}
|
||||
|
||||
void _hideSlides() {
|
||||
for (final s in slides) {
|
||||
s.classes.add('next-hidden');
|
||||
}
|
||||
}
|
||||
|
||||
void _initBullets() {
|
||||
final bulletContainer = DivElement();
|
||||
bulletContainer.classes.add('bullet-container');
|
||||
|
||||
for (var i = 0; i < slides.length; i++) {
|
||||
final bullet = DivElement();
|
||||
bullet.classes.add('bullet');
|
||||
bullet.id = 'bullet-index-$i';
|
||||
bullet.onClick.listen((e) => _goToIndexSlide(i));
|
||||
bulletContainer.append(bullet);
|
||||
}
|
||||
|
||||
container.append(bulletContainer);
|
||||
}
|
||||
|
||||
void _initArrows() {
|
||||
final prevArrow = AnchorElement();
|
||||
final iPrev = DivElement();
|
||||
iPrev.classes.addAll(['fa', 'fa-chevron-left', 'fa-lg']);
|
||||
prevArrow.classes.add('slider-left');
|
||||
prevArrow.append(iPrev);
|
||||
prevArrow.onClick.listen((e) => _slideLeft());
|
||||
|
||||
final nextArrow = AnchorElement();
|
||||
final iNext = DivElement();
|
||||
iNext.classes.addAll(['fa', 'fa-chevron-right', 'fa-lg']);
|
||||
nextArrow.classes.add('slider-right');
|
||||
nextArrow.append(iNext);
|
||||
nextArrow.onClick.listen((e) => _slideRight());
|
||||
|
||||
container.append(prevArrow);
|
||||
container.append(nextArrow);
|
||||
}
|
||||
|
||||
void _touchStartListener(TouchEvent e) {
|
||||
x0 = e.changedTouches!.first.client.x;
|
||||
touched = true;
|
||||
}
|
||||
|
||||
void _touchEndListener(TouchEvent e) {
|
||||
if (touched) {
|
||||
int dx = (e.changedTouches!.first.client.x - x0) as int;
|
||||
|
||||
// dx==0 case is ignored
|
||||
if (dx > 0 && currentSlideIndex > 0) {
|
||||
_slideLeft();
|
||||
} else if (dx < 0 && currentSlideIndex < lastSlideIndex) {
|
||||
_slideRight();
|
||||
}
|
||||
touched = false;
|
||||
}
|
||||
}
|
||||
|
||||
void _initGestureListener() {
|
||||
container.onTouchStart.listen(_touchStartListener);
|
||||
container.onTouchEnd.listen(_touchEndListener);
|
||||
}
|
||||
|
||||
void _updateBullets() {
|
||||
final bullets =
|
||||
querySelector('.bullet-container')!.querySelectorAll('.bullet');
|
||||
for (var i = 0; i < bullets.length; i++) {
|
||||
bullets[i].classes.remove('active');
|
||||
if (i == currentSlideIndex) {
|
||||
bullets[i].classes.add('active');
|
||||
}
|
||||
}
|
||||
_checkRepeat();
|
||||
}
|
||||
|
||||
void _checkRepeat() {
|
||||
var prevArrow = querySelector('.slider-left') as AnchorElement;
|
||||
var nextArrow = querySelector('.slider-right') as AnchorElement;
|
||||
|
||||
if (currentSlideIndex == slides.length - 1) {
|
||||
slides[0].classes.add('hidden');
|
||||
slides[slides.length - 1].classes.remove('hidden');
|
||||
prevArrow.classes.remove('hidden');
|
||||
nextArrow.classes.add('hidden');
|
||||
} else if (currentSlideIndex == 0) {
|
||||
slides[slides.length - 1].classes.add('hidden');
|
||||
slides[0].classes.remove('hidden');
|
||||
prevArrow.classes.add('hidden');
|
||||
nextArrow.classes.remove('hidden');
|
||||
} else {
|
||||
slides[slides.length - 1].classes.remove('hidden');
|
||||
slides[0].classes.remove('hidden');
|
||||
prevArrow.classes.remove('hidden');
|
||||
nextArrow.classes.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
void _slideRight() {
|
||||
if (currentSlideIndex < lastSlideIndex) {
|
||||
currentSlideIndex++;
|
||||
} else {
|
||||
currentSlideIndex = 0;
|
||||
}
|
||||
|
||||
if (currentSlideIndex > 0) {
|
||||
prevSlide = slides[currentSlideIndex - 1];
|
||||
} else {
|
||||
prevSlide = slides[lastSlideIndex];
|
||||
}
|
||||
|
||||
currentSlide = slides[currentSlideIndex];
|
||||
|
||||
if (currentSlideIndex < lastSlideIndex) {
|
||||
nextSlide = slides[currentSlideIndex + 1];
|
||||
} else {
|
||||
nextSlide = slides[0];
|
||||
}
|
||||
|
||||
for (final e in slides) {
|
||||
_removeSlideClasses([e]);
|
||||
if (e.classes.contains('prev-hidden')) e.classes.add('next-hidden');
|
||||
if (e.classes.contains('prev')) e.classes.add('prev-hidden');
|
||||
}
|
||||
|
||||
_removeSlideClasses([prevSlide, currentSlide, nextSlide]);
|
||||
|
||||
prevSlide.classes.add('prev');
|
||||
currentSlide.classes.add('active');
|
||||
nextSlide.classes.add('next');
|
||||
|
||||
_updateBullets();
|
||||
}
|
||||
|
||||
void _slideLeft() {
|
||||
if (currentSlideIndex > 0) {
|
||||
currentSlideIndex--;
|
||||
} else {
|
||||
currentSlideIndex = lastSlideIndex;
|
||||
}
|
||||
|
||||
if (currentSlideIndex < lastSlideIndex) {
|
||||
nextSlide = slides[currentSlideIndex + 1];
|
||||
} else {
|
||||
nextSlide = slides[0];
|
||||
}
|
||||
|
||||
currentSlide = slides[currentSlideIndex];
|
||||
|
||||
if (currentSlideIndex > 0) {
|
||||
prevSlide = slides[currentSlideIndex - 1];
|
||||
} else {
|
||||
prevSlide = slides[lastSlideIndex];
|
||||
}
|
||||
|
||||
for (final e in slides) {
|
||||
_removeSlideClasses([e]);
|
||||
if (e.classes.contains('next')) e.classes.add('next-hidden');
|
||||
if (e.classes.contains('next-hidden')) e.classes.add('prev-hidden');
|
||||
}
|
||||
|
||||
_removeSlideClasses([prevSlide, currentSlide, nextSlide]);
|
||||
|
||||
prevSlide.classes.add('prev');
|
||||
currentSlide.classes.add('active');
|
||||
nextSlide.classes.add('next');
|
||||
|
||||
_updateBullets();
|
||||
}
|
||||
|
||||
void _goToIndexSlide(int index) {
|
||||
final sliding =
|
||||
(currentSlideIndex < index) ? () => _slideRight() : () => _slideLeft();
|
||||
while (currentSlideIndex != index) {
|
||||
sliding();
|
||||
}
|
||||
}
|
||||
|
||||
void _removeSlideClasses(List<Element> slides) {
|
||||
for (final s in slides) {
|
||||
s.classes
|
||||
.removeAll(['prev-hidden', 'prev', 'active', 'next', 'next-hidden']);
|
||||
}
|
||||
}
|
||||
|
||||
void _initArrowKeyControl() {
|
||||
Element.keyUpEvent.forTarget(document.body).listen((e) {
|
||||
if (e.keyCode == KeyCode.LEFT && currentSlideIndex > 0) {
|
||||
_slideLeft();
|
||||
}
|
||||
if (e.keyCode == KeyCode.RIGHT && currentSlideIndex < lastSlideIndex) {
|
||||
_slideRight();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,169 +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
|
||||
|
||||
/// Defines the data types for this project.
|
||||
library;
|
||||
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
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<dynamic, dynamic> 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. At least 1 screenshot is required.
|
||||
final List<Screenshot> screenshots;
|
||||
|
||||
/// A link to the source code.
|
||||
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;
|
||||
|
||||
/// The type of the sample. Supported values are either 'sample' or
|
||||
/// 'demo'.
|
||||
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({
|
||||
required this.name,
|
||||
this.author = 'Flutter',
|
||||
required this.screenshots,
|
||||
required this.source,
|
||||
this.web,
|
||||
required this.description,
|
||||
this.difficulty = 'beginner',
|
||||
this.widgets = const [],
|
||||
this.packages = const [],
|
||||
this.tags = const [],
|
||||
this.platforms = const [],
|
||||
required this.type,
|
||||
this.date,
|
||||
this.channel,
|
||||
});
|
||||
|
||||
factory Sample.fromJson(Map<dynamic, dynamic> json) => _$SampleFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$SampleToJson(this);
|
||||
|
||||
String get thumbnail {
|
||||
var screenshotUrl = screenshots.first.url;
|
||||
var prefix = path.dirname(screenshotUrl);
|
||||
var filename = path.basenameWithoutExtension(screenshotUrl);
|
||||
return path.join(prefix, '${filename}_thumb.png');
|
||||
}
|
||||
|
||||
String get searchAttributes {
|
||||
var buf = StringBuffer();
|
||||
buf.write(name.toLowerCase());
|
||||
buf.write(' ');
|
||||
|
||||
for (final tag in tags) {
|
||||
buf.write('tag:${tag.toLowerCase()} ');
|
||||
|
||||
// Allow tags to be searched without the tag: prefix
|
||||
buf.write('${tag.toLowerCase()} ');
|
||||
}
|
||||
|
||||
for (final platform in platforms) {
|
||||
buf.write('platform:$platform ');
|
||||
|
||||
// Allow platforms to be searched without the tag: prefix
|
||||
buf.write('$platform ');
|
||||
}
|
||||
|
||||
for (final widget in widgets) {
|
||||
buf.write('widget:$widget ');
|
||||
}
|
||||
|
||||
for (final 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<dynamic, dynamic> json) =>
|
||||
_$ScreenshotFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$ScreenshotToJson(this);
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'data.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Index _$IndexFromJson(Map json) => $checkedCreate(
|
||||
'Index',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = Index(
|
||||
$checkedConvert(
|
||||
'samples',
|
||||
(v) => (v as List<dynamic>)
|
||||
.map((e) => Sample.fromJson(e as Map))
|
||||
.toList()),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$IndexToJson(Index instance) => <String, dynamic>{
|
||||
'samples': instance.samples,
|
||||
};
|
||||
|
||||
Sample _$SampleFromJson(Map json) => $checkedCreate(
|
||||
'Sample',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = Sample(
|
||||
name: $checkedConvert('name', (v) => v as String),
|
||||
author: $checkedConvert('author', (v) => v as String? ?? 'Flutter'),
|
||||
screenshots: $checkedConvert(
|
||||
'screenshots',
|
||||
(v) => (v as List<dynamic>)
|
||||
.map((e) => Screenshot.fromJson(e as Map))
|
||||
.toList()),
|
||||
source: $checkedConvert('source', (v) => v as String),
|
||||
web: $checkedConvert('web', (v) => v as String?),
|
||||
description: $checkedConvert('description', (v) => v as String),
|
||||
difficulty:
|
||||
$checkedConvert('difficulty', (v) => v as String? ?? 'beginner'),
|
||||
widgets: $checkedConvert(
|
||||
'widgets',
|
||||
(v) =>
|
||||
(v as List<dynamic>?)?.map((e) => e as String).toList() ??
|
||||
const []),
|
||||
packages: $checkedConvert(
|
||||
'packages',
|
||||
(v) =>
|
||||
(v as List<dynamic>?)?.map((e) => e as String).toList() ??
|
||||
const []),
|
||||
tags: $checkedConvert(
|
||||
'tags',
|
||||
(v) =>
|
||||
(v as List<dynamic>?)?.map((e) => e as String).toList() ??
|
||||
const []),
|
||||
platforms: $checkedConvert(
|
||||
'platforms',
|
||||
(v) =>
|
||||
(v as List<dynamic>?)?.map((e) => e as String).toList() ??
|
||||
const []),
|
||||
type: $checkedConvert('type', (v) => v as String),
|
||||
date: $checkedConvert(
|
||||
'date', (v) => v == null ? null : DateTime.parse(v as String)),
|
||||
channel: $checkedConvert('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,
|
||||
'type': instance.type,
|
||||
'date': instance.date?.toIso8601String(),
|
||||
'channel': instance.channel,
|
||||
};
|
||||
|
||||
Screenshot _$ScreenshotFromJson(Map json) => $checkedCreate(
|
||||
'Screenshot',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = Screenshot(
|
||||
$checkedConvert('url', (v) => v as String),
|
||||
$checkedConvert('alt', (v) => v as String),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ScreenshotToJson(Screenshot instance) =>
|
||||
<String, dynamic>{
|
||||
'url': instance.url,
|
||||
'alt': instance.alt,
|
||||
};
|
@ -1,513 +0,0 @@
|
||||
samples:
|
||||
- name: Material 3
|
||||
author: Flutter
|
||||
screenshots:
|
||||
- url: images/material_3_components.png
|
||||
alt: Components tab of the Material 3 demo
|
||||
- url: images/material_3_color.png
|
||||
alt: Colors tab of the Material 3 demo
|
||||
- url: images/material_3_typography.png
|
||||
alt: Typography tab of the Material 3 demo
|
||||
- url: images/material_3_elevation.png
|
||||
alt: Elevation tab of the Material 3 demo
|
||||
- url: images/material_3_green.png
|
||||
alt: Elevation tab of the Material 3 demo with seed color of green
|
||||
source: https://github.com/flutter/samples/tree/main/material_3_demo
|
||||
description: >
|
||||
Showcases Material 3 features in the Flutter Material library.
|
||||
These features include updated components, typography, color system and elevation support.
|
||||
difficulty: beginner
|
||||
widgets:
|
||||
- Theme
|
||||
- TextButton
|
||||
- ElevatedButton
|
||||
- OutlinedButton
|
||||
- Text
|
||||
- Card
|
||||
- AppBar
|
||||
packages: [ ]
|
||||
tags: [ "material", "design", "gallery" ]
|
||||
platforms: [ "ios", "android", "web", "windows", "macos", "linux" ]
|
||||
type: demo
|
||||
web: web/material_3_demo
|
||||
|
||||
- name: Rich Text Editor
|
||||
author: Flutter
|
||||
screenshots:
|
||||
- url: images/simple_editor_active.png
|
||||
alt: Advanced text editing with activity
|
||||
- url: images/simple_editor_initial.png
|
||||
alt: Advanced text editing in initial state
|
||||
source: https://github.com/flutter/samples/tree/main/simplistic_editor
|
||||
description: >
|
||||
This is a fancy text editor sample which shows how to consume fine-grain
|
||||
text editing and selection details from the framework's TextEditingDeltas
|
||||
APIs.
|
||||
difficulty: advanced
|
||||
widgets:
|
||||
- TextInput
|
||||
packages: []
|
||||
tags: ["demo", "text"]
|
||||
platforms: ["ios", "android", "web", "windows", "macos", "linux"]
|
||||
type: demo
|
||||
web: web/simplistic_editor
|
||||
|
||||
- name: Web Embedding
|
||||
author: Flutter and Angular
|
||||
screenshots:
|
||||
- url: images/web_embedding1.png
|
||||
alt: A Flutter app embedded in an Angular app
|
||||
- url: images/web_embedding2.png
|
||||
alt: A Flutter app embedded in an Angular app
|
||||
source: https://github.com/flutter/samples/tree/main/web_embedding
|
||||
description: >
|
||||
An example app showing how to embed Flutter in a web application using Angular
|
||||
difficulty: advanced
|
||||
widgets: []
|
||||
packages: []
|
||||
platforms: ['web']
|
||||
tags: ['demo', 'web', 'add-to-app', 'embedding']
|
||||
web: https://flutter-angular.web.app/
|
||||
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/main/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: Code Sharing
|
||||
author: Flutter
|
||||
screenshots:
|
||||
- url: images/code_sharing.jpg
|
||||
alt: Counter app communicating with server
|
||||
source: https://github.com/flutter/samples/tree/main/code_sharing
|
||||
description: >
|
||||
Demonstrates simple way to share business logic between a Flutter app and
|
||||
a server running Dart.
|
||||
difficulty: intermediate
|
||||
packages:
|
||||
- freezed
|
||||
- shelf
|
||||
tags: ['intermediate', 'sample', 'code-sharing', 'dart', 'server']
|
||||
platforms: ['android', 'ios', 'linux', 'macos', 'web', 'windows']
|
||||
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/main/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/main/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/main/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: 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/main/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/main/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/main/platform_view_swift
|
||||
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: Infinite List
|
||||
author: Flutter
|
||||
screenshots:
|
||||
- url: images/infinite_list.png
|
||||
alt: Infinite List screenshot
|
||||
source: https://github.com/flutter/samples/tree/main/infinite_list
|
||||
description: >
|
||||
A Flutter sample app that shows an implementation of the "infinite list" UX pattern.
|
||||
That is, a list is shown to the user as if it was continuous although it is internally
|
||||
paginated. This is a common feature of mobile apps, from shopping catalogs
|
||||
through search engines to social media clients.
|
||||
difficulty: intermediate
|
||||
widgets:
|
||||
- Selector
|
||||
- AppBar
|
||||
- ListTile
|
||||
- ListView
|
||||
packages:
|
||||
- provider
|
||||
- meta
|
||||
tags: ['sample', 'material', 'design', 'android', 'ios']
|
||||
platforms: ['ios', 'android']
|
||||
type: sample
|
||||
|
||||
- name: IOS App Clip
|
||||
author: Flutter
|
||||
screenshots:
|
||||
- url: images/ios_app_clip.png
|
||||
alt: IOS App Clip screenshot
|
||||
source: https://github.com/flutter/samples/tree/main/ios_app_clip
|
||||
description: >
|
||||
A Flutter sample app that shows the demonstrating integration with iOS 14's App Clip,
|
||||
the App Clip target is rendered by Flutter and uses a plugin.
|
||||
difficulty: intermediate
|
||||
widgets:
|
||||
- CupertinoApp
|
||||
- AppBar
|
||||
- FlutterLogo
|
||||
packages:
|
||||
- device_info
|
||||
tags: ['sample', 'Device Info', 'ios']
|
||||
platforms: ['ios']
|
||||
type: sample
|
||||
|
||||
- name: Testing App
|
||||
author: Flutter
|
||||
screenshots:
|
||||
- url: images/testing_app1.png
|
||||
alt: Testing App screenshot
|
||||
- url: images/testing_app2.png
|
||||
alt: Testing App screenshot
|
||||
source: https://github.com/flutter/samples/tree/main/testing_app
|
||||
description: >
|
||||
A Flutter sample app that shows different types of testing in Flutter.
|
||||
difficulty: intermediate
|
||||
widgets:
|
||||
- AppBar
|
||||
- ListTile
|
||||
- ListView
|
||||
- Snackbar
|
||||
packages:
|
||||
- provider
|
||||
tags: ['sample', 'material', 'android', 'ios']
|
||||
platforms: ['ios', 'android']
|
||||
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/main/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
|
||||
|
||||
- name: Web Dashboard
|
||||
author: Flutter
|
||||
screenshots:
|
||||
- url: images/web_dashboard1.png
|
||||
alt: Web Dashboard screenshot
|
||||
- url: images/web_dashboard2.png
|
||||
alt: Web Dashboard screenshot
|
||||
- url: images/web_dashboard3.png
|
||||
alt: Web Dashboard screenshot
|
||||
source: https://github.com/flutter/samples/tree/main/experimental/web_dashboard
|
||||
description: >
|
||||
A dashboard app that displays daily entries. Demonstrates AdaptiveScaffold and NavigationRail. Showcases how to
|
||||
use Firebase, but uses a mock backend by default.
|
||||
difficulty: advanced
|
||||
widgets:
|
||||
- AdaptiveScaffold
|
||||
- NavigationRail
|
||||
- FutureBuilder
|
||||
- StreamBuilder
|
||||
packages:
|
||||
- firebase
|
||||
tags: ['intermediate', 'sample', 'firebase']
|
||||
platforms: ['ios', 'android', 'web']
|
||||
type: sample
|
||||
web: web/web_dashboard
|
||||
|
||||
- name: Form App
|
||||
author: Flutter
|
||||
screenshots:
|
||||
- url: images/form_app1.png
|
||||
alt: Form App screenshot
|
||||
- url: images/form_app2.png
|
||||
alt: Form App screenshot
|
||||
- url: images/form_app3.png
|
||||
alt: Form App screenshot
|
||||
source: https://github.com/flutter/samples/tree/main/form_app
|
||||
description: >
|
||||
A Flutter sample app that shows how to use Forms.
|
||||
difficulty: intermediate
|
||||
widgets:
|
||||
- Form
|
||||
packages: []
|
||||
tags: ['intermediate', 'sample', 'forms']
|
||||
platforms: ['ios', 'android', 'web']
|
||||
type: sample
|
||||
web: web/form_app
|
||||
|
||||
- name: Navigation and Routing
|
||||
author: Flutter
|
||||
screenshots:
|
||||
- url: images/navigation_and_routing1.png
|
||||
alt: Navigation and Routing screenshot
|
||||
- url: images/navigation_and_routing2.png
|
||||
alt: Navigation and Routing screenshot
|
||||
- url: images/navigation_and_routing3.png
|
||||
alt: Navigation and Routing screenshot
|
||||
- url: images/navigation_and_routing4.png
|
||||
alt: Navigation and Routing screenshot
|
||||
source: https://github.com/flutter/samples/tree/main/navigation_and_routing
|
||||
description: >
|
||||
A Flutter sample app that shows how to use how to use the Router API to
|
||||
handle common navigation scenarios.
|
||||
difficulty: advanced
|
||||
widgets:
|
||||
- Router
|
||||
packages: []
|
||||
tags: ['advanced', 'sample', 'navigation', 'router']
|
||||
platforms: ['ios', 'android', 'web']
|
||||
type: sample
|
||||
web: web/navigation_and_routing
|
||||
|
||||
- name: Photo Search app
|
||||
author: Flutter
|
||||
screenshots:
|
||||
- url: images/desktop_photo_search-fluent_ui.png
|
||||
alt: Desktop Photo Search with FluentUI widgets
|
||||
- url: images/desktop_photo_search-material.png
|
||||
alt: Desktop Photo Search with Material widgets
|
||||
source: https://github.com/flutter/samples/tree/main/desktop_photo_search
|
||||
description: >
|
||||
This is the Photo Search app, built out with two different widget sets,
|
||||
`material` shows the Photo Search app built with Material widgets, and
|
||||
`fluent_ui` shows the Photo Search app built with Fluent UI widgets.
|
||||
difficulty: medium
|
||||
widgets: []
|
||||
packages:
|
||||
- built_collection
|
||||
- built_value
|
||||
- file_selector
|
||||
- fluent_ui
|
||||
- flutter/material
|
||||
- provider
|
||||
- url_launcher
|
||||
tags: ['desktop', 'rest-api']
|
||||
platforms: ['windows', 'macos', 'linux']
|
||||
type: sample
|
||||
|
||||
- name: Slide Puzzle
|
||||
author: Very Good Ventures
|
||||
screenshots:
|
||||
- url: images/slide_puzzle1.png
|
||||
alt: Slide Puzzle screenshot
|
||||
source: https://github.com/VGVentures/slide_puzzle
|
||||
description: >
|
||||
A slide puzzle built for Flutter Challenge.
|
||||
difficulty: advanced
|
||||
widgets: []
|
||||
packages: []
|
||||
platforms: ['web']
|
||||
tags: ['demo', 'game']
|
||||
type: demo
|
||||
|
||||
- name: Game Template
|
||||
author: Flutter
|
||||
screenshots:
|
||||
- url: images/loading_screen.png
|
||||
alt: Loading screen
|
||||
- url: images/level_selector.png
|
||||
alt: Level selection screen
|
||||
source: https://github.com/flutter/samples/tree/main/game_template
|
||||
description: >
|
||||
This is a game template that shows how to build much of the dressing
|
||||
around an actual game. The game itself is very simple - the value in this
|
||||
sample is everything around the game.
|
||||
difficulty: beginner
|
||||
widgets:
|
||||
- GoRouter
|
||||
- AppLifecycleObserver
|
||||
packages:
|
||||
- audioplayers
|
||||
- firebase_crashlytics
|
||||
- games_services
|
||||
- go_router
|
||||
- google_mobile_ads
|
||||
- in_app_purchase
|
||||
- logging
|
||||
- provider
|
||||
- shared_preferences
|
||||
tags: ["games", "firebase", "ads", "crashlytics", "routing"]
|
||||
platforms: ["ios", "android", "web", "windows", "macos", "linux"]
|
||||
type: demo
|
||||
web: web/game_template
|
||||
|
||||
- name: Dice
|
||||
author: Jaime Blasco
|
||||
screenshots:
|
||||
- url: images/dice.png
|
||||
alt: Dice screenshot
|
||||
source: https://github.com/jamesblasco/zflutter/blob/master/zflutter/example/lib/examples/dice/dice.dart
|
||||
description: >
|
||||
A demo of 3d animation using dice
|
||||
difficulty: advanced
|
||||
widgets: []
|
||||
packages: []
|
||||
platforms: ['web']
|
||||
tags: ['demo', 'animation']
|
||||
web: https://z.flutter.gallery/#/dice
|
||||
type: demo
|
@ -1,84 +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
|
||||
|
||||
bool matchesQuery(String query, String sampleAttributes) {
|
||||
if (query.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var queryWords = query.toLowerCase().split(' ')
|
||||
..removeWhere((s) => s.isEmpty);
|
||||
var attributes = sampleAttributes.toLowerCase().split(' ')
|
||||
..removeWhere((s) => s.isEmpty);
|
||||
|
||||
// Test for type filter
|
||||
// This will check whether a type parameter is present in the
|
||||
// search query, and return false if the self type mismatches
|
||||
// the query type
|
||||
for (final word in queryWords) {
|
||||
if ((word.contains('type:') && !attributes.contains(word)) ||
|
||||
(word.contains('platform:') && !attributes.contains('type:demo'))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Test for exact matches
|
||||
if (attributes.contains(query)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Test for exact matches for keywords
|
||||
var matches = 0;
|
||||
for (final 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 (final attribute in attributes) {
|
||||
for (final 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(' ');
|
||||
var value = params['type'];
|
||||
if (value != null) buf.write('type:$value');
|
||||
}
|
||||
if (params.containsKey('platform')) {
|
||||
if (buf.isNotEmpty) buf.write(' ');
|
||||
var value = params['platform'];
|
||||
if (value != null) buf.write('platform:$value');
|
||||
}
|
||||
|
||||
return buf.toString();
|
||||
}
|
@ -1,233 +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:convert';
|
||||
|
||||
import 'data.dart';
|
||||
import 'util.dart' as util;
|
||||
|
||||
String _escapeAttribute(String s) =>
|
||||
const HtmlEscape(HtmlEscapeMode.attribute).convert(s);
|
||||
String _escapeElement(String s) =>
|
||||
const 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>
|
||||
''';
|
||||
|
||||
const 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=Google+Sans|Google+Sans+Display|Roboto:300,400,500&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>
|
||||
$_googleAnalytics
|
||||
</head>
|
||||
''';
|
||||
|
||||
const 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=Google+Sans|Google+Sans+Display|Roboto:300,400,500&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 src="https://kit.fontawesome.com/16cc04762e.js"></script>
|
||||
<script defer src="description.dart.js"></script>
|
||||
$_googleAnalytics
|
||||
</head>
|
||||
''';
|
||||
|
||||
const String _navbar = '''
|
||||
<div class="navbar">
|
||||
<a class="leading" href="./">
|
||||
<img src="images/logos/logo_lockup_flutter_horizontal_wht_96.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 ${DateTime.now().toUtc().year}</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">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--z0" 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.thumbnail)}"></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="slider-container">
|
||||
<div class="slider-content">
|
||||
${util.indent(_descriptionScreenshots(sample), 4)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="description">
|
||||
${util.indent(_descriptionText(sample), 4)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
''';
|
||||
|
||||
String _descriptionButtons(Sample sample) {
|
||||
var buf = StringBuffer();
|
||||
var sampleLink = sample.web;
|
||||
if (sampleLink != null && sampleLink.isNotEmpty) {
|
||||
buf.write(
|
||||
'''<button class="mdc-button mdc-button--outlined" onclick="window.location.href = '$sampleLink';"><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>''');
|
||||
}
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
String _tags(Sample sample) {
|
||||
var buf = StringBuffer();
|
||||
for (final 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 (final screenshot in sample.screenshots) {
|
||||
buf.write(
|
||||
'''<div class="slider-single"><img class="slider-single-image" src="${screenshot.url}" alt="${screenshot.alt}" /></div>\n''');
|
||||
}
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
String _descriptionText(Sample sample) {
|
||||
return '<p>${sample.description}</p>';
|
||||
}
|
||||
|
||||
const String _googleAnalytics = """
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-67589403-8"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'UA-67589403-8');
|
||||
</script>""";
|
@ -1,35 +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: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 group = match.group(0);
|
||||
if (group == null) return input;
|
||||
var lower = group.toLowerCase();
|
||||
|
||||
if (match.start > 0) {
|
||||
lower = '$separator$lower';
|
||||
}
|
||||
|
||||
return lower;
|
||||
});
|
@ -1,40 +0,0 @@
|
||||
name: samples_index
|
||||
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.5.0
|
||||
|
||||
dependencies:
|
||||
checked_yaml: ^2.0.3
|
||||
json_annotation: ^4.8.1
|
||||
mdc_web: ^0.6.0
|
||||
path: ^1.8.3
|
||||
sass_builder: ^2.2.1
|
||||
web: ^1.1.0
|
||||
yaml: ^3.1.2
|
||||
|
||||
dev_dependencies:
|
||||
build: ^2.4.0
|
||||
build_runner: ^2.4.2
|
||||
build_web_compilers: ^4.0.3
|
||||
grinder: ^0.9.4
|
||||
image: ^4.1.3
|
||||
json_serializable: ^6.6.2
|
||||
lints: ^5.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:
|
||||
|
||||
# https://github.com/material-components/material-components-web/pull/7158
|
||||
#
|
||||
# Until then, dart-sass produces a warning that this operator is being removed
|
||||
# in favor of calc().
|
||||
#
|
||||
# See this issue for details:
|
||||
# https://github.com/dart-lang/dart-pad/issues/2388
|
||||
dependency_overrides:
|
||||
sass: ^1.62.0
|
@ -1,165 +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:checked_yaml/checked_yaml.dart';
|
||||
import 'package:samples_index/browser.dart';
|
||||
import 'package:samples_index/samples_index.dart';
|
||||
import 'package:test/test.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) => m != null ? Index.fromJson(m) : null,
|
||||
sourceUrl: file.uri);
|
||||
if (index == null) {
|
||||
throw ('unable to load YAML from $file');
|
||||
}
|
||||
expect(index.samples, isNotEmpty);
|
||||
|
||||
var sample = index.samples.first;
|
||||
expect(sample, isNotNull);
|
||||
expect(sample.name, 'Kittens');
|
||||
expect(sample.screenshots, hasLength(2));
|
||||
expect(sample.source, 'https://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.type, 'sample');
|
||||
expect(sample.date, DateTime.parse('2019-12-15T02:59:43.1Z'));
|
||||
expect(sample.channel, 'stable');
|
||||
});
|
||||
});
|
||||
|
||||
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) => m != null ? Index.fromJson(m) : null,
|
||||
sourceUrl: file.uri);
|
||||
if (index == null) {
|
||||
throw ('unable to load YAML from $file');
|
||||
}
|
||||
var sample = index.samples.first;
|
||||
expect(
|
||||
sample.searchAttributes.split(' '),
|
||||
containsAll(const <String>[
|
||||
'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 '
|
||||
'type:sample';
|
||||
|
||||
// Test if various queries match these attributes
|
||||
expect(matchesQuery('foo', attributes), false);
|
||||
expect(matchesQuery('Foo', attributes), false);
|
||||
expect(matchesQuery('kittens', attributes), true);
|
||||
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);
|
||||
|
||||
// Test if queries match by type
|
||||
expect(matchesQuery('type:sample', attributes), true);
|
||||
expect(matchesQuery('type:demo', attributes), false);
|
||||
expect(matchesQuery('kittens type:demo', 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=demo'), containsPair('type', 'demo'));
|
||||
});
|
||||
|
||||
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(''));
|
||||
});
|
||||
});
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
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 or app
|
||||
date: 2019-12-15T02:59:43.1Z
|
||||
channel: stable
|
@ -1,21 +0,0 @@
|
||||
samples:
|
||||
- name: Kittens
|
||||
screenshots:
|
||||
- url: https://placekitten.com/500/500
|
||||
alt: a kitten
|
||||
- url: https://placekitten.com/400/400
|
||||
alt: another kitten
|
||||
source: https://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']
|
||||
type: sample # sample or app
|
||||
date: 2019-12-15T02:59:43.1Z
|
||||
channel: stable
|
@ -1,98 +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:grinder/grinder.dart';
|
||||
import 'package:image/image.dart' as image;
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:samples_index/samples_index.dart';
|
||||
import 'package:samples_index/src/templates.dart' as templates;
|
||||
|
||||
Future<void> main(List<String> args) => grind(args);
|
||||
|
||||
@Task('Run tests in the VM')
|
||||
Future<void> testCli() async =>
|
||||
await TestRunner().testAsync(platformSelector: 'vm');
|
||||
|
||||
@Task()
|
||||
void analyze() {
|
||||
run('dart', arguments: const ['analyze', '--fatal-infos', '.']);
|
||||
}
|
||||
|
||||
@Task('deploy')
|
||||
@Depends(analyze, testCli, generate, buildRelease)
|
||||
void deploy() {
|
||||
log('All tasks completed. ');
|
||||
log('');
|
||||
}
|
||||
|
||||
@Task('Run build_runner to public/ directory')
|
||||
@Depends(createThumbnails)
|
||||
Future<void> buildRelease() async {
|
||||
var app = PubApp.local('build_runner');
|
||||
await app.runAsync(
|
||||
'build --release --output web:public --delete-conflicting-outputs'
|
||||
.split(' ')
|
||||
.toList());
|
||||
}
|
||||
|
||||
@DefaultTask('Build the project.')
|
||||
@Depends(clean)
|
||||
Future<void> generate() async {
|
||||
var samples = await getSamples();
|
||||
log('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<void>>[];
|
||||
for (final 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<void>(futures);
|
||||
log('Generated index for ${samples.length} samples.');
|
||||
}
|
||||
|
||||
@Task('creates thumbnail images in web/images')
|
||||
Future<void> createThumbnails() async {
|
||||
await _createThumbnails(Directory('web/images'));
|
||||
}
|
||||
|
||||
// Creates a thumbnail image for each png file
|
||||
Future<void> _createThumbnails(Directory directory) async {
|
||||
var files = await directory.list().toList();
|
||||
var filesToWrite = <Future<void>>{};
|
||||
|
||||
for (final entity in files) {
|
||||
var extension = path.extension(entity.path);
|
||||
var filename = path.basenameWithoutExtension(entity.path);
|
||||
if (extension != '.png' || entity is! File || filename.endsWith('_thumb')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var pathPrefix = path.dirname(entity.path);
|
||||
var thumbnailFile = File(path.join(pathPrefix, '${filename}_thumb.png'));
|
||||
|
||||
var img = image.decodeImage(await entity.readAsBytes());
|
||||
var resized = image.copyResize(img!, width: 640);
|
||||
filesToWrite.add(thumbnailFile.writeAsBytes(image.encodePng(resized)));
|
||||
}
|
||||
|
||||
await Future.wait<void>(filesToWrite);
|
||||
}
|
||||
|
||||
@Task('remove generated HTML files')
|
||||
Future<void> clean() async {
|
||||
var tasks = <Future<void>>[];
|
||||
await for (final file in Directory('web').list(recursive: true)) {
|
||||
if (path.extension(file.path) == '.html') {
|
||||
tasks.add(file.delete());
|
||||
}
|
||||
}
|
||||
await Future.wait<void>(tasks);
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
// TODO(kevmoo): https://github.com/flutter/samples/issues/2582
|
||||
// ignore: deprecated_member_use
|
||||
import 'dart:html';
|
||||
|
||||
import 'package:mdc_web/mdc_web.dart';
|
||||
import 'package:samples_index/src/carousel.dart';
|
||||
|
||||
void main() {
|
||||
querySelectorAll('.mdc-card__primary-action').forEach((el) => MDCRipple(el));
|
||||
|
||||
// Initialize carousel
|
||||
// This carousel will use the div slider-container as the base
|
||||
// withArrowKeyControl is used to listen for arrow key up events
|
||||
Carousel.init(withArrowKeyControl: true);
|
||||
}
|
Before Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 146 KiB |
Before Width: | Height: | Size: 110 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 574 KiB |
Before Width: | Height: | Size: 99 KiB |
Before Width: | Height: | Size: 112 KiB |
Before Width: | Height: | Size: 272 KiB |
Before Width: | Height: | Size: 736 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 920 KiB |
Before Width: | Height: | Size: 846 KiB |
Before Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 83 KiB |
Before Width: | Height: | Size: 451 KiB |
Before Width: | Height: | Size: 301 KiB |
Before Width: | Height: | Size: 1.6 MiB |
Before Width: | Height: | Size: 3.2 MiB |
Before Width: | Height: | Size: 717 KiB |
Before Width: | Height: | Size: 263 KiB |
Before Width: | Height: | Size: 220 KiB |
Before Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 139 KiB |
Before Width: | Height: | Size: 115 KiB |
Before Width: | Height: | Size: 164 KiB |
Before Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 99 KiB |