Add workflow to deploy the sample index (#791)

* Add workflow to build and deploy the sample index

* update gh-pages action

* fix yaml

* create web/ directory in build

* grammar

* add ignored directories

* revert pubspec.lock files

* add job to run _tool/verify_samples.dart

* Update filipino_cuisine for Flutter 2

* remove timeflow demo.

The unnamed List constructor is now deprecated, refactoring
this code to use add() requires more knowledge about the code for
this demo.
https://dart.dev/null-safety/understanding-null-safety#no-unnamed-list-constructor

* update slide_puzzle

* ensure stable channel is used to verify

* move verify web demos action into separate yaml file - avoid running
on each flutter version.

* add on: pull_request

* update slide_puzzle

* Update gh-pages.yml

* Add copyright header
pull/796/head
John Ryan 3 years ago committed by GitHub
parent b26f2cccc1
commit 3f5ab56485
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,31 @@
name: Deploy to GitHub Pages
on:
push:
branches:
- master
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: true
fetch-depth: 0
- uses: subosito/flutter-action@v1
- name: Init scripts
run: dart pub get
working-directory: web/_tool
- name: Build
run: dart _tool/build_ci.dart
working-directory: web
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: web/samples_index/public

@ -0,0 +1,20 @@
name: Verify web demos
on: [push, pull_request]
jobs:
verify-web-demos:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: true
fetch-depth: 0
- uses: subosito/flutter-action@v1
with:
channel: stable
- name: Init scripts
run: dart pub get
working-directory: web/_tool
- name: Verify packages
run: dart _tool/verify_packages.dart
working-directory: web

@ -0,0 +1,43 @@
// 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';
final ignoredDirectories = ['_tool', 'samples_index'];
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', 'pub', ['get']);
await run('samples_index', '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']);
await run(directory, 'mv', [sourceBuildDir, targetDirectory]);
}
}

@ -36,3 +36,16 @@ Future<bool> run(
void logWrapped(int code, String message) {
print('\x1B[${code}m$message\x1B[0m');
}
Iterable<String> listPackageDirs(Directory dir) sync* {
if (File('${dir.path}/pubspec.yaml').existsSync()) {
yield dir.path;
} else {
for (var subDir in dir
.listSync(followLinks: true)
.whereType<Directory>()
.where((d) => !Uri.file(d.path).pathSegments.last.startsWith('.'))) {
yield* listPackageDirs(subDir);
}
}
}

@ -5,14 +5,11 @@
import 'dart:io';
import 'package:path/path.dart' as p;
import 'common.dart';
const _ansiGreen = 32;
const _ansiRed = 31;
const _ansiMagenta = 35;
import 'common.dart';
void main() async {
final packageDirs = _listPackageDirs(Directory.current)
final packageDirs = listPackageDirs(Directory.current)
.map((path) => p.relative(path, from: Directory.current.path))
.toList();
@ -21,7 +18,7 @@ void main() async {
final results = <bool>[];
for (var i = 0; i < packageDirs.length; i++) {
final dir = packageDirs[i];
logWrapped(_ansiMagenta, '\n$dir (${i + 1} of ${packageDirs.length})');
logWrapped(ansiMagenta, '\n$dir (${i + 1} of ${packageDirs.length})');
results.add(await run(dir, 'flutter', [
'pub',
'pub',
@ -46,19 +43,6 @@ void _printStatus(List<bool> results) {
var success = (successCount == results.length);
var pct = 100 * successCount / results.length;
logWrapped(success ? _ansiGreen : _ansiRed,
logWrapped(success ? ansiGreen : ansiRed,
'$successCount of ${results.length} (${pct.toStringAsFixed(2)}%)');
}
Iterable<String> _listPackageDirs(Directory dir) sync* {
if (File('${dir.path}/pubspec.yaml').existsSync()) {
yield dir.path;
} else {
for (var subDir in dir
.listSync(followLinks: true)
.whereType<Directory>()
.where((d) => !Uri.file(d.path).pathSegments.last.startsWith('.'))) {
yield* _listPackageDirs(subDir);
}
}
}

@ -14,7 +14,7 @@ class CState extends State<Cook> {
initState() {
super.initState();
cb = List();
cb = [];
}
Widget build(ct) {

@ -60,7 +60,7 @@ class CatmullInterpolator implements Interpolator {
}
static void test() {
List<Point2D> controlPoints = List<Point2D>();
List<Point2D> controlPoints = <Point2D>[];
controlPoints.add(Point2D(-1, 1));
controlPoints.add(Point2D(0, 1));
controlPoints.add(Point2D(1, -1));

@ -47,12 +47,12 @@ class LayeredChartState extends State<LayeredChart> {
graphHeight = MathUtils.clampedMap(screenRatio, 0.5, 2.5, 50, 150);
int m = dataToPlot.length;
paths = List<Path>(m);
capPaths = List<Path>(m);
maxValues = List<double>(m);
paths = <Path>[];
capPaths = <Path>[];
maxValues = <double>[];
for (int i = 0; i < m; i++) {
int n = dataToPlot[i].series.length;
maxValues[i] = 0;
maxValues.add(0);
for (int j = 0; j < n; j++) {
double v = dataToPlot[i].series[j].toDouble();
if (v > maxValues[i]) {
@ -69,11 +69,11 @@ class LayeredChartState extends State<LayeredChart> {
double xWidth = (endX - startX) / numPoints;
double capRangeX = capSize * cos(capTheta);
double tanCapTheta = tan(capTheta);
List<double> curvePoints = List<double>(numPoints);
List<double> curvePoints = <double>[];
for (int i = 0; i < m; i++) {
List<int> series = dataToPlot[i].series;
int n = series.length;
List<Point2D> controlPoints = List<Point2D>();
List<Point2D> controlPoints = <Point2D>[];
controlPoints.add(Point2D(-1, 0));
double last = 0;
for (int j = 0; j < n; j++) {
@ -88,11 +88,11 @@ class LayeredChartState extends State<LayeredChart> {
cpv.value = MathUtils.map(
j.toDouble(), 0, (numPoints - 1).toDouble(), 0, (n - 1).toDouble());
curve.progressiveGet(cpv);
curvePoints[j] = MathUtils.map(
max(0, cpv.value), 0, maxValues[i].toDouble(), 0, graphHeight);
curvePoints.add(MathUtils.map(
max(0, cpv.value), 0, maxValues[i].toDouble(), 0, graphHeight));
}
paths[i] = Path();
capPaths[i] = Path();
paths.add(Path());
capPaths.add(Path());
paths[i].moveTo(startX, startY);
capPaths[i].moveTo(startX, startY);
for (int j = 0; j < numPoints; j++) {
@ -133,7 +133,7 @@ class LayeredChartState extends State<LayeredChart> {
capPaths[i].lineTo(startX, startY + 1);
capPaths[i].close();
}
labelPainter = List<TextPainter>();
labelPainter = <TextPainter>[];
for (int i = 0; i < dataToPlot.length; i++) {
TextSpan span = TextSpan(
style: TextStyle(
@ -146,7 +146,7 @@ class LayeredChartState extends State<LayeredChart> {
tp.layout();
labelPainter.add(tp);
}
milestonePainter = List<TextPainter>();
milestonePainter = <TextPainter>[];
for (int i = 0; i < milestones.length; i++) {
TextSpan span = TextSpan(
style: TextStyle(

@ -46,7 +46,7 @@ class _MainLayoutState extends State<MainLayout> with TickerProviderStateMixin {
createAnimation(0);
weekLabels = List();
weekLabels = <WeekLabel>[];
weekLabels.add(WeekLabel.forDate(DateTime(2019, 2, 26), "v1.2"));
weekLabels.add(WeekLabel.forDate(DateTime(2018, 12, 4), "v1.0"));
// weekLabels.add(WeekLabel.forDate(new DateTime(2018, 9, 19), "Preview 2"));
@ -79,9 +79,9 @@ class _MainLayoutState extends State<MainLayout> with TickerProviderStateMixin {
@override
Widget build(BuildContext context) {
// Combined contributions data
List<DataSeries> dataToPlot = List();
List<DataSeries> dataToPlot = [];
if (contributions != null) {
List<int> series = List();
List<int> series = [];
for (UserContribution userContrib in contributions) {
for (int i = 0; i < userContrib.contributions.length; i++) {
ContributionData data = userContrib.contributions[i];
@ -228,7 +228,7 @@ class _MainLayoutState extends State<MainLayout> with TickerProviderStateMixin {
List<StatForWeek> summarizeWeeksFromTSV(
String statByWeekStr, int numWeeksTotal) {
List<StatForWeek> loadedStats = List();
List<StatForWeek> loadedStats = [];
HashMap<int, StatForWeek> statMap = HashMap();
statByWeekStr.split("\n").forEach((s) {
List<String> split = s.split("\t");
@ -237,7 +237,8 @@ class _MainLayoutState extends State<MainLayout> with TickerProviderStateMixin {
statMap[weekNum] = StatForWeek(weekNum, int.parse(split[1]));
}
});
print("Laoded ${statMap.length} weeks.");
print("Loaded ${statMap.length} weeks.");
// Convert into a list by week, but fill in empty weeks with 0
for (int i = 0; i < numWeeksTotal; i++) {
StatForWeek starsForWeek = statMap[i];

@ -7,7 +7,7 @@ packages:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.5"
version: "1.1.0"
charcode:
dependency: transitive
description:
@ -21,7 +21,7 @@ packages:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0-nullsafety.5"
version: "1.15.0"
flutter:
dependency: "direct main"
description: flutter
@ -54,7 +54,7 @@ packages:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.6"
version: "1.3.0"
path:
dependency: transitive
description:
@ -101,13 +101,13 @@ packages:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.5"
version: "1.3.0"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.5"
version: "2.1.0"
sdks:
dart: ">=2.12.0-0 <3.0.0"

@ -8,7 +8,6 @@ directories:
- github_dataviz/web
- particle_background/web
- slide_puzzle/web
- timeflow/web
- form_app/web
- web_dashboard/web
- place_tracker/web

@ -22,10 +22,8 @@ You should see a message printing the URL to access: `http://localhost:8080`
## Deploying to GitHub Pages
This project uses [peanut][peanut] to build the samples and commit the output
to the gh-pages branch. To deploy, run these commands in the `web/` directory:
Install the peanut command:
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

@ -508,22 +508,6 @@ samples:
web: web/slide_puzzle
type: demo
- name: Timeflow
author: Fabian Stein
screenshots:
- url: images/timeflow1.png
alt: Timeflow screenshot
source: https://github.com/Fabian-Stein/timeflow
description: >
A gentle animation that provides a calming experience to stressed developers.
difficulty: advanced
widgets: []
packages: []
platforms: ['web']
tags: ['demo', 'animation']
web: web/timeflow
type: demo
- name: Dice
author: Jaime Blasco
screenshots:

@ -19,7 +19,7 @@ void testCli() async => await TestRunner().testAsync(platformSelector: 'vm');
@Task()
void analyze() {
PubApp.local('tuneup')..run(['check']);
PubApp.local('tuneup').run(['check']);
}
@Task('deploy')

@ -1 +1 @@
Subproject commit 5ef5526acb58f9ffb6f3fb22118cbd825613dc73
Subproject commit 5c590d0b0252cf3d7cbddf9998d7807b87c91550

@ -1,13 +0,0 @@
Copyright 2019 Fabian Stein
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
(the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -1,103 +0,0 @@
A gentle animation that provides a calming experience to stressed developers.
Contributed as part of the Flutter Create 5K challenge by Fabian Stein.
Original source at
[github.com/Fabian-Stein/timeflow](https://github.com/Fabian-Stein/timeflow).
Timers and stopwatches arent the most peaceful gadgets, often reminding us of
urgent tasks, deadlines and unpleasant appointments. Not in this case, Timeflow
is the epitome of pure tranquility, ideal for mindful activities: mediation,
yoga or exercise. The slow, breath like animation is free of sudden, abrupt
jumps and builds up to a Zen finish.
## Use
Tap the screen to start/pause the timer
when paused:
1. red button reset the timer and the animation
2. green button: resume the timer
when finished/startscreen:
1. blue button choose the desired timeframe
2. orange button randomize a new triangle mesh/color scheme
## Code description
Please run dartfmt for readability.
Some of the variable names are short and I have not used comments, because of the character limit, so here is an explanation.
### globals
triangles: the list of triangles that are animated
percent: how much of the timer is completed (from 0.0 to 1.0)
cTime: the time that is already gone by since the start of the timer (paused time is excluded)
dur: how long is the timer in Milliseconds
rng: the random number generator that is used throughout the program
rebuild: is an indicator that the triangles destination points should be rebuild
### class TM
The timer class that manages the state of the app
SI cState: tracks the change of the apps state: is the timer stopped, playing or paused
pTime: tracks when the ticker was paused
Ticker t: the ticker that calls the update function up every frame
up: function that updates the current time or stops the timer, when the duration is reached
press, pause, play, stop: callback functions, for the button presses
openDialog: callback function, opens the numberPickerDialog, which is used to pick the timer duration
build: returns the app, mainly the custom painter P is called
### class P
The custom painter, which draws the triangles
paint:
d = diameter of the circle is 2/3 of the width of the screen
1. if the triangles are not setup completely (rebuild == true) calculate the outer points of for every triangle setupdP this happens here, because the ratio of the screens has got be known
2. paint all triangles
shouldRepaint: every frame should be repainted
### class T
The triangle class
sP: the list of the starting points of the triangle (these are the points you see at the start of the animation)
dP: the list of destination points (the outer points, where the triangles wander to first, before they circle back to the starting point)
constructor: p1,p2,p3 are the starting points, c is the overall color scheme (blue, red, green etc.)
for the triangle a random color out of the color scheme is chosen: p.color = c[100 * (rng.nextInt(9) + 1)];
the rest of the function determines, if the triangle is in the circle, if it is, it is added to the triangles list, otherwise it is forgotten and should be freed by the garbage collector
setupdP: setup the destination points, choose a random x and y position on the screen
cP: gives back the current points of the triangle with respect to the timerstate, some trigonometry and interpolations happen here
this is responsible for the animations
1. alter the alpha repetitively:
2. alter the distance to the starting points, use a linear interpolation between the starting points sP and the destination points dP with respect to the percentage already done
3. alter the angle with respect to the starting points
4. alter the size of the triangles repetitively
### function setupT
setup the Triangles (starting positions + color scheme)
dim: dimensions of the “net”
1. make a net of points in the following manner:
. . . . .
. . . .
. . . . .
. . . .
. . . . .
. . . .
2. alter the points a little bit by randomization, so that the net is a little more intresting
3. connect the points to form triangles
4. randomize a color scheme for the triangles

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

@ -1,264 +0,0 @@
// Package infinite_listview:
// https://pub.dartlang.org/packages/infinite_listview
import 'dart:math' as math;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
/// Infinite ListView
///
/// ListView that builds its children with to an infinite extent.
///
class InfiniteListView extends StatelessWidget {
/// See [ListView.builder]
InfiniteListView.builder({
Key key,
this.scrollDirection = Axis.vertical,
this.reverse = false,
InfiniteScrollController controller,
this.physics,
this.padding,
this.itemExtent,
@required IndexedWidgetBuilder itemBuilder,
int itemCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
this.cacheExtent,
}) : positiveChildrenDelegate = SliverChildBuilderDelegate(
itemBuilder,
childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
),
negativeChildrenDelegate = SliverChildBuilderDelegate(
(BuildContext context, int index) => itemBuilder(context, -1 - index),
childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
),
controller = controller ?? InfiniteScrollController(),
super(key: key);
/// See [ListView.separated]
InfiniteListView.separated({
Key key,
this.scrollDirection = Axis.vertical,
this.reverse = false,
InfiniteScrollController controller,
this.physics,
this.padding,
@required IndexedWidgetBuilder itemBuilder,
@required IndexedWidgetBuilder separatorBuilder,
int itemCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
this.cacheExtent,
}) : assert(itemBuilder != null),
assert(separatorBuilder != null),
itemExtent = null,
positiveChildrenDelegate = SliverChildBuilderDelegate(
(BuildContext context, int index) {
final itemIndex = index ~/ 2;
return index.isEven
? itemBuilder(context, itemIndex)
: separatorBuilder(context, itemIndex);
},
childCount: itemCount != null ? math.max(0, itemCount * 2 - 1) : null,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
),
negativeChildrenDelegate = SliverChildBuilderDelegate(
(BuildContext context, int index) {
final itemIndex = (-1 - index) ~/ 2;
return index.isOdd
? itemBuilder(context, itemIndex)
: separatorBuilder(context, itemIndex);
},
childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
),
controller = controller ?? InfiniteScrollController(),
super(key: key);
/// See: [ScrollView.scrollDirection]
final Axis scrollDirection;
/// See: [ScrollView.reverse]
final bool reverse;
/// See: [ScrollView.controller]
final InfiniteScrollController controller;
/// See: [ScrollView.physics]
final ScrollPhysics physics;
/// See: [BoxScrollView.padding]
final EdgeInsets padding;
/// See: [ListView.itemExtent]
final double itemExtent;
/// See: [ScrollView.cacheExtent]
final double cacheExtent;
/// See: [ListView.childrenDelegate]
final SliverChildDelegate negativeChildrenDelegate;
/// See: [ListView.childrenDelegate]
final SliverChildDelegate positiveChildrenDelegate;
@override
Widget build(BuildContext context) {
final List<Widget> slivers = _buildSlivers(context, negative: false);
final List<Widget> negativeSlivers = _buildSlivers(context, negative: true);
final AxisDirection axisDirection = _getDirection(context);
final scrollPhysics = AlwaysScrollableScrollPhysics(parent: physics);
return Scrollable(
axisDirection: axisDirection,
controller: controller,
physics: scrollPhysics,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return Builder(builder: (BuildContext context) {
/// Build negative [ScrollPosition] for the negative scrolling [Viewport].
final state = Scrollable.of(context);
final negativeOffset = _InfiniteScrollPosition(
physics: scrollPhysics,
context: state,
initialPixels: -offset.pixels,
keepScrollOffset: controller.keepScrollOffset,
);
/// Keep the negative scrolling [Viewport] positioned to the [ScrollPosition].
offset.addListener(() {
negativeOffset._forceNegativePixels(offset.pixels);
});
/// Stack the two [Viewport]s on top of each other so they move in sync.
return Stack(
children: <Widget>[
Viewport(
axisDirection: flipAxisDirection(axisDirection),
anchor: 1.0,
offset: negativeOffset,
slivers: negativeSlivers,
cacheExtent: cacheExtent,
),
Viewport(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
cacheExtent: cacheExtent,
),
],
);
});
},
);
}
AxisDirection _getDirection(BuildContext context) {
return getAxisDirectionFromAxisReverseAndDirectionality(
context, scrollDirection, reverse);
}
List<Widget> _buildSlivers(BuildContext context, {bool negative = false}) {
Widget sliver;
if (itemExtent != null) {
sliver = SliverFixedExtentList(
delegate:
negative ? negativeChildrenDelegate : positiveChildrenDelegate,
itemExtent: itemExtent,
);
} else {
sliver = SliverList(
delegate:
negative ? negativeChildrenDelegate : positiveChildrenDelegate);
}
if (padding != null) {
sliver = new SliverPadding(
padding: negative
? padding - EdgeInsets.only(bottom: padding.bottom)
: padding - EdgeInsets.only(top: padding.top),
sliver: sliver,
);
}
return <Widget>[sliver];
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new EnumProperty<Axis>('scrollDirection', scrollDirection));
properties.add(new FlagProperty('reverse',
value: reverse, ifTrue: 'reversed', showName: true));
properties.add(new DiagnosticsProperty<ScrollController>(
'controller', controller,
showName: false, defaultValue: null));
properties.add(new DiagnosticsProperty<ScrollPhysics>('physics', physics,
showName: false, defaultValue: null));
properties.add(new DiagnosticsProperty<EdgeInsetsGeometry>(
'padding', padding,
defaultValue: null));
properties
.add(new DoubleProperty('itemExtent', itemExtent, defaultValue: null));
properties.add(
new DoubleProperty('cacheExtent', cacheExtent, defaultValue: null));
}
}
/// Same as a [ScrollController] except it provides [ScrollPosition] objects with infinite bounds.
class InfiniteScrollController extends ScrollController {
/// Creates a new [InfiniteScrollController]
InfiniteScrollController({
double initialScrollOffset = 0.0,
bool keepScrollOffset = true,
String debugLabel,
}) : super(
initialScrollOffset: initialScrollOffset,
keepScrollOffset: keepScrollOffset,
debugLabel: debugLabel,
);
@override
ScrollPosition createScrollPosition(ScrollPhysics physics,
ScrollContext context, ScrollPosition oldPosition) {
return new _InfiniteScrollPosition(
physics: physics,
context: context,
initialPixels: initialScrollOffset,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
);
}
}
class _InfiniteScrollPosition extends ScrollPositionWithSingleContext {
_InfiniteScrollPosition({
@required ScrollPhysics physics,
@required ScrollContext context,
double initialPixels = 0.0,
bool keepScrollOffset = true,
ScrollPosition oldPosition,
String debugLabel,
}) : super(
physics: physics,
context: context,
initialPixels: initialPixels,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
);
void _forceNegativePixels(double value) {
super.forcePixels(-value);
}
@override
double get minScrollExtent => double.negativeInfinity;
@override
double get maxScrollExtent => double.infinity;
}

@ -1,260 +0,0 @@
import 'dart:core';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'numberpicker.dart';
main() => runApp(MaterialApp(home: App(), debugShowCheckedModeBanner: false));
class App extends StatefulWidget {
@override
State<StatefulWidget> createState() => TM();
}
enum SI { pause, play, stop }
List<T> triangles;
var percent = 0.0, cTime = 0.0, dur = 120000.0, rng = Random(), rebuild = true;
class TM extends State<App> {
SI cState = SI.stop;
Ticker t;
var pTime = 0.0;
@override
initState() {
// Screen.keepOn(true);
t = Ticker(up);
super.initState();
}
up(Duration d) {
if (cState == SI.play) {
setState(() {
if (cTime >= dur)
stop();
else {
cTime = d.inMilliseconds.toDouble() + pTime;
percent = cTime / dur;
}
});
}
}
press() {
if (cState == SI.play)
pause();
else if (cState == SI.pause)
play();
else {
cState = SI.play;
t.start();
}
}
pause() {
setState(() {
cState = SI.pause;
t.stop();
});
}
play() {
setState(() {
cState = SI.play;
t.start();
pTime = cTime;
});
}
stop() {
setState(() {
cState = SI.stop;
t.stop();
pTime = 0.0;
cTime = 0.0;
percent = 0.0;
});
}
openDialog() {
showDialog<num>(
context: context,
builder: (BuildContext context) {
return NumberPickerDialog.integer(
initialIntegerValue: (dur + 1.0) ~/ 60000,
maxValue: 20,
minValue: 1,
title: Text('Minutes'));
}).then((num v) {
if (v != null) dur = 60000.0 * v;
});
}
@override
Widget build(BuildContext context) {
List<Widget> w = List();
if (cState == SI.pause) {
w.add(fab(Colors.green, play, Icons.play_arrow));
w.add(SizedBox(height: 10));
w.add(fab(Colors.red, stop, Icons.close));
w.add(SizedBox(height: 20));
}
if (cState == SI.stop) {
w.add(fab(Colors.lightBlue, openDialog, Icons.timer));
w.add(SizedBox(height: 10));
w.add(fab(Colors.yellow[900], () {
rebuild = true;
}, Icons.loop));
w.add(SizedBox(height: 20));
}
Column r = Column(mainAxisAlignment: MainAxisAlignment.end, children: w);
return Scaffold(
backgroundColor: Colors.black,
body: SizedBox.expand(
child: Container(
child: CustomPaint(
painter: P(),
child: TextButton(
onPressed: press,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [r]))))));
}
}
FloatingActionButton fab(Color c, VoidCallback f, IconData ic) =>
FloatingActionButton(backgroundColor: c, onPressed: f, child: Icon(ic));
class P extends CustomPainter {
@override
paint(Canvas canvas, Size size) {
var w = size.width, h = size.height, d = 2 / 3 * w;
if (w > 0.1 && h > 0.1) {
if (rebuild) {
rebuild = false;
setupT();
for (var t in triangles) t.setupdP(w / d, h / d);
}
for (var t in triangles) {
var cP = t.cP(), p = Path();
p.moveTo(cP[0].x * d + w / 2, cP[0].y * d + h / 2);
for (i = 1; i < 3; i++)
p.lineTo(cP[i].x * d + w / 2, cP[i].y * d + h / 2);
p.close();
canvas.drawPath(p, t.p);
}
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
int i;
class T {
List<Point> dP = List(3), sP = List(3);
Paint p;
T(Point p1, p2, p3, var c) {
p = Paint()..style = PaintingStyle.fill;
sP[0] = p1;
sP[1] = p2;
sP[2] = p3;
p.color = c[100 * (rng.nextInt(9) + 1)];
double x = 0, y = 0;
for (i = 0; i < 3; i++) {
x += sP[i].x;
y += sP[i].y;
}
x = 2 * x / 3;
y = 2 * y / 3;
if (x * x + y * y < 1) triangles.add(this);
}
setupdP(double wR, hR) {
var x = (rng.nextDouble() - 0.5) * (wR - 0.1),
y = (rng.nextDouble() - 0.5) * (hR - 0.1);
dP[0] = Point(x, y);
for (i = 1; i < 3; i++)
dP[i] = Point(sP[i].x + x - sP[0].x, sP[i].y + y - sP[0].y);
}
List<Point> cP() {
List<Point> res = List(3);
var p, k, o = 6000, r;
if (cTime < o)
p = 1 - cTime / o;
else
p = (cTime - o) / (dur - o);
k = 2 * ((cTime.toInt() % o) - o / 2).abs() / o;
r = min(min(1, (dur - cTime) / o), cTime / o);
this.p.color = this.p.color.withAlpha(255 - (200 * k * r).toInt());
for (i = 0; i < 3; i++)
res[i] = Point(
sP[i].x * p + dP[i].x * (1 - p), sP[i].y * p + dP[i].y * (1 - p));
if (cTime > o) {
var d = res[0].distanceTo(sP[0]);
var a = acos((sP[0].x - res[0].x) / d);
if (sP[0].y > res[0].y) a = 2 * pi - a;
var b = pi - a + p * pi * dur / 120000;
var dX = cos(b) * d, dY = sin(b) * d;
for (i = 0; i < 3; i++) res[i] = Point(sP[i].x + dX, sP[i].y + dY);
}
double mx = 0, my = 0;
for (i = 0; i < 3; i++) {
mx += res[i].x;
my += res[i].y;
}
mx /= 3;
my /= 3;
for (i = 0; i < 3; i++)
res[i] = Point(res[i].x + (res[i].x - mx) * (1 - k) * r / 2,
res[i].y + (res[i].y - my) * (1 - k) * r / 2);
return res;
}
}
setupT() {
int dim = 20, x, y;
List<Point> tri = List(dim * dim);
for (x = 0; x < dim; x++) {
for (y = 0; y < dim; y++) {
var dx = rng.nextDouble() - 0.5, dy = rng.nextDouble() - 0.5, off;
if (x % 2 == 0)
off = 0;
else
off = 0.5;
tri[x * dim + y] =
Point((x + dx) / (dim - 1) - 0.5, (y + off + dy) / (dim - 1) - 0.5);
}
}
triangles = List();
var r = rng.nextInt(5), c;
if (r == 0) c = Colors.lightBlue;
if (r == 1) c = Colors.yellow;
if (r == 2) c = Colors.lightGreen;
if (r == 3) c = Colors.red;
if (r == 4) c = Colors.pink;
for (x = 0; x < dim - 1; x++) {
for (y = 0; y < dim - 1; y++) {
int off = x * dim;
T(tri[y + off], tri[y + 1 + off], tri[y + off + dim], c);
T(tri[y + off + dim], tri[y + 1 + off], tri[y + 1 + off + dim], c);
}
}
}

@ -1,527 +0,0 @@
// Package numberpicker:
// https://pub.dartlang.org/packages/numberpicker
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'infinite_listview.dart';
/// Created by Marcin Szałek
///NumberPicker is a widget designed to pick a number between #minValue and #maxValue
class NumberPicker extends StatelessWidget {
///height of every list element
static const double DEFAULT_ITEM_EXTENT = 50.0;
///width of list view
static const double DEFAULT_LISTVIEW_WIDTH = 100.0;
///constructor for integer number picker
NumberPicker.integer({
Key key,
@required int initialValue,
@required this.minValue,
@required this.maxValue,
@required this.onChanged,
this.itemExtent = DEFAULT_ITEM_EXTENT,
this.listViewWidth = DEFAULT_LISTVIEW_WIDTH,
this.step = 1,
this.infiniteLoop = false,
}) : assert(initialValue != null),
assert(minValue != null),
assert(maxValue != null),
assert(maxValue > minValue),
assert(initialValue >= minValue && initialValue <= maxValue),
assert(step > 0),
selectedIntValue = initialValue,
selectedDecimalValue = -1,
decimalPlaces = 0,
intScrollController = infiniteLoop
? new InfiniteScrollController(
initialScrollOffset:
(initialValue - minValue) ~/ step * itemExtent,
)
: new ScrollController(
initialScrollOffset:
(initialValue - minValue) ~/ step * itemExtent,
),
decimalScrollController = null,
_listViewHeight = 3 * itemExtent,
integerItemCount = (maxValue - minValue) ~/ step + 1,
super(key: key);
///constructor for decimal number picker
NumberPicker.decimal({
Key key,
@required double initialValue,
@required this.minValue,
@required this.maxValue,
@required this.onChanged,
this.decimalPlaces = 1,
this.itemExtent = DEFAULT_ITEM_EXTENT,
this.listViewWidth = DEFAULT_LISTVIEW_WIDTH,
}) : assert(initialValue != null),
assert(minValue != null),
assert(maxValue != null),
assert(decimalPlaces != null && decimalPlaces > 0),
assert(maxValue > minValue),
assert(initialValue >= minValue && initialValue <= maxValue),
selectedIntValue = initialValue.floor(),
selectedDecimalValue = ((initialValue - initialValue.floorToDouble()) *
math.pow(10, decimalPlaces))
.round(),
intScrollController = new ScrollController(
initialScrollOffset: (initialValue.floor() - minValue) * itemExtent,
),
decimalScrollController = new ScrollController(
initialScrollOffset: ((initialValue - initialValue.floorToDouble()) *
math.pow(10, decimalPlaces))
.roundToDouble() *
itemExtent,
),
_listViewHeight = 3 * itemExtent,
step = 1,
integerItemCount = maxValue.floor() - minValue.floor() + 1,
infiniteLoop = false,
super(key: key);
///called when selected value changes
final ValueChanged<num> onChanged;
///min value user can pick
final int minValue;
///max value user can pick
final int maxValue;
///inidcates how many decimal places to show
/// e.g. 0=>[1,2,3...], 1=>[1.0, 1.1, 1.2...] 2=>[1.00, 1.01, 1.02...]
final int decimalPlaces;
///height of every list element in pixels
final double itemExtent;
///view will always contain only 3 elements of list in pixels
final double _listViewHeight;
///width of list view in pixels
final double listViewWidth;
///ScrollController used for integer list
final ScrollController intScrollController;
///ScrollController used for decimal list
final ScrollController decimalScrollController;
///Currently selected integer value
final int selectedIntValue;
///Currently selected decimal value
final int selectedDecimalValue;
///Step between elements. Only for integer datePicker
///Examples:
/// if step is 100 the following elements may be 100, 200, 300...
/// if min=0, max=6, step=3, then items will be 0, 3 and 6
/// if min=0, max=5, step=3, then items will be 0 and 3.
final int step;
///Repeat values infinitely
final bool infiniteLoop;
///Amount of items
final int integerItemCount;
//
//----------------------------- PUBLIC ------------------------------
//
animateInt(int valueToSelect) {
int diff = valueToSelect - minValue;
int index = diff ~/ step;
animateIntToIndex(index);
}
animateIntToIndex(int index) {
_animate(intScrollController, index * itemExtent);
}
animateDecimal(int decimalValue) {
_animate(decimalScrollController, decimalValue * itemExtent);
}
animateDecimalAndInteger(double valueToSelect) {
animateInt(valueToSelect.floor());
animateDecimal(((valueToSelect - valueToSelect.floorToDouble()) *
math.pow(10, decimalPlaces))
.round());
}
//
//----------------------------- VIEWS -----------------------------
//
///main widget
@override
Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context);
if (infiniteLoop) {
return _integerInfiniteListView(themeData);
}
if (decimalPlaces == 0) {
return _integerListView(themeData);
} else {
return new Row(
children: <Widget>[
_integerListView(themeData),
_decimalListView(themeData),
],
mainAxisAlignment: MainAxisAlignment.center,
);
}
}
Widget _integerListView(ThemeData themeData) {
TextStyle defaultStyle = themeData.textTheme.bodyText2;
TextStyle selectedStyle =
themeData.textTheme.headline5.copyWith(color: themeData.accentColor);
var listItemCount = integerItemCount + 2;
return new NotificationListener(
child: new Container(
height: _listViewHeight,
width: listViewWidth,
child: new ListView.builder(
controller: intScrollController,
itemExtent: itemExtent,
itemCount: listItemCount,
cacheExtent: _calculateCacheExtent(listItemCount),
itemBuilder: (BuildContext context, int index) {
final int value = _intValueFromIndex(index);
//define special style for selected (middle) element
final TextStyle itemStyle =
value == selectedIntValue ? selectedStyle : defaultStyle;
bool isExtra = index == 0 || index == listItemCount - 1;
return isExtra
? new Container() //empty first and last element
: new Center(
child: new Text(value.toString(), style: itemStyle),
);
},
),
),
onNotification: _onIntegerNotification,
);
}
Widget _decimalListView(ThemeData themeData) {
TextStyle defaultStyle = themeData.textTheme.bodyText2;
TextStyle selectedStyle =
themeData.textTheme.headline5.copyWith(color: themeData.accentColor);
int decimalItemCount =
selectedIntValue == maxValue ? 3 : math.pow(10, decimalPlaces) + 2;
return new NotificationListener(
child: new Container(
height: _listViewHeight,
width: listViewWidth,
child: new ListView.builder(
controller: decimalScrollController,
itemExtent: itemExtent,
itemCount: decimalItemCount,
itemBuilder: (BuildContext context, int index) {
final int value = index - 1;
//define special style for selected (middle) element
final TextStyle itemStyle =
value == selectedDecimalValue ? selectedStyle : defaultStyle;
bool isExtra = index == 0 || index == decimalItemCount - 1;
return isExtra
? new Container() //empty first and last element
: new Center(
child: new Text(
value.toString().padLeft(decimalPlaces, '0'),
style: itemStyle),
);
},
),
),
onNotification: _onDecimalNotification,
);
}
Widget _integerInfiniteListView(ThemeData themeData) {
TextStyle defaultStyle = themeData.textTheme.bodyText2;
TextStyle selectedStyle =
themeData.textTheme.headline5.copyWith(color: themeData.accentColor);
return new NotificationListener(
child: new Container(
height: _listViewHeight,
width: listViewWidth,
child: new InfiniteListView.builder(
controller: intScrollController,
itemExtent: itemExtent,
itemBuilder: (BuildContext context, int index) {
final int value = _intValueFromIndex(index);
//define special style for selected (middle) element
final TextStyle itemStyle =
value == selectedIntValue ? selectedStyle : defaultStyle;
return new Center(
child: new Text(value.toString(), style: itemStyle),
);
},
),
),
onNotification: _onIntegerNotification,
);
}
//
// ----------------------------- LOGIC -----------------------------
//
int _intValueFromIndex(int index) {
index--;
index %= integerItemCount;
return minValue + index * step;
}
bool _onIntegerNotification(Notification notification) {
if (notification is ScrollNotification) {
//calculate
int intIndexOfMiddleElement =
(notification.metrics.pixels / itemExtent).round();
if (!infiniteLoop) {
intIndexOfMiddleElement =
intIndexOfMiddleElement.clamp(0, integerItemCount - 1);
}
int intValueInTheMiddle = _intValueFromIndex(intIndexOfMiddleElement + 1);
intValueInTheMiddle = _normalizeIntegerMiddleValue(intValueInTheMiddle);
if (_userStoppedScrolling(notification, intScrollController)) {
//center selected value
animateIntToIndex(intIndexOfMiddleElement);
}
//update selection
if (intValueInTheMiddle != selectedIntValue) {
num newValue;
if (decimalPlaces == 0) {
//return integer value
newValue = (intValueInTheMiddle);
} else {
if (intValueInTheMiddle == maxValue) {
//if new value is maxValue, then return that value and ignore decimal
newValue = (intValueInTheMiddle.toDouble());
animateDecimal(0);
} else {
//return integer+decimal
double decimalPart = _toDecimal(selectedDecimalValue);
newValue = ((intValueInTheMiddle + decimalPart).toDouble());
}
}
onChanged(newValue);
}
}
return true;
}
bool _onDecimalNotification(Notification notification) {
if (notification is ScrollNotification) {
//calculate middle value
int indexOfMiddleElement =
(notification.metrics.pixels + _listViewHeight / 2) ~/ itemExtent;
int decimalValueInTheMiddle = indexOfMiddleElement - 1;
decimalValueInTheMiddle =
_normalizeDecimalMiddleValue(decimalValueInTheMiddle);
if (_userStoppedScrolling(notification, decimalScrollController)) {
//center selected value
animateDecimal(decimalValueInTheMiddle);
}
//update selection
if (selectedIntValue != maxValue &&
decimalValueInTheMiddle != selectedDecimalValue) {
double decimalPart = _toDecimal(decimalValueInTheMiddle);
double newValue = ((selectedIntValue + decimalPart).toDouble());
onChanged(newValue);
}
}
return true;
}
///There was a bug, when if there was small integer range, e.g. from 1 to 5,
///When user scrolled to the top, whole listview got displayed.
///To prevent this we are calculating cacheExtent by our own so it gets smaller if number of items is smaller
double _calculateCacheExtent(int itemCount) {
double cacheExtent = 250.0; //default cache extent
if ((itemCount - 2) * DEFAULT_ITEM_EXTENT <= cacheExtent) {
cacheExtent = ((itemCount - 3) * DEFAULT_ITEM_EXTENT);
}
return cacheExtent;
}
///When overscroll occurs on iOS,
///we can end up with value not in the range between [minValue] and [maxValue]
///To avoid going out of range, we change values out of range to border values.
int _normalizeMiddleValue(int valueInTheMiddle, int min, int max) {
return math.max(math.min(valueInTheMiddle, max), min);
}
int _normalizeIntegerMiddleValue(int integerValueInTheMiddle) {
//make sure that max is a multiple of step
int max = (maxValue ~/ step) * step;
return _normalizeMiddleValue(integerValueInTheMiddle, minValue, max);
}
int _normalizeDecimalMiddleValue(int decimalValueInTheMiddle) {
return _normalizeMiddleValue(
decimalValueInTheMiddle, 0, math.pow(10, decimalPlaces) - 1);
}
///indicates if user has stopped scrolling so we can center value in the middle
bool _userStoppedScrolling(
Notification notification, ScrollController scrollController) {
return notification is UserScrollNotification &&
notification.direction == ScrollDirection.idle &&
// ignore: invalid_use_of_protected_member,invalid_use_of_visible_for_testing_member
scrollController.position.activity is! HoldScrollActivity;
}
///converts integer indicator of decimal value to double
///e.g. decimalPlaces = 1, value = 4 >>> result = 0.4
/// decimalPlaces = 2, value = 12 >>> result = 0.12
double _toDecimal(int decimalValueAsInteger) {
return double.parse((decimalValueAsInteger * math.pow(10, -decimalPlaces))
.toStringAsFixed(decimalPlaces));
}
///scroll to selected value
_animate(ScrollController scrollController, double value) {
scrollController.animateTo(value,
duration: new Duration(seconds: 1), curve: new ElasticOutCurve());
}
}
///Returns AlertDialog as a Widget so it is designed to be used in showDialog method
class NumberPickerDialog extends StatefulWidget {
final int minValue;
final int maxValue;
final int initialIntegerValue;
final double initialDoubleValue;
final int decimalPlaces;
final Widget title;
final EdgeInsets titlePadding;
final Widget confirmWidget;
final Widget cancelWidget;
final int step;
final bool infiniteLoop;
///constructor for integer values
NumberPickerDialog.integer({
@required this.minValue,
@required this.maxValue,
@required this.initialIntegerValue,
this.title,
this.titlePadding,
this.step = 1,
this.infiniteLoop = false,
Widget confirmWidget,
Widget cancelWidget,
}) : confirmWidget = confirmWidget ?? new Text("OK"),
cancelWidget = cancelWidget ?? new Text("CANCEL"),
decimalPlaces = 0,
initialDoubleValue = -1.0;
///constructor for decimal values
NumberPickerDialog.decimal({
@required this.minValue,
@required this.maxValue,
@required this.initialDoubleValue,
this.decimalPlaces = 1,
this.title,
this.titlePadding,
Widget confirmWidget,
Widget cancelWidget,
}) : confirmWidget = confirmWidget ?? new Text("OK"),
cancelWidget = cancelWidget ?? new Text("CANCEL"),
initialIntegerValue = -1,
step = 1,
infiniteLoop = false;
@override
State<NumberPickerDialog> createState() =>
new _NumberPickerDialogControllerState(
initialIntegerValue, initialDoubleValue);
}
class _NumberPickerDialogControllerState extends State<NumberPickerDialog> {
int selectedIntValue;
double selectedDoubleValue;
_NumberPickerDialogControllerState(
this.selectedIntValue, this.selectedDoubleValue);
_handleValueChanged(num value) {
if (value is int) {
setState(() => selectedIntValue = value);
} else {
setState(() => selectedDoubleValue = value);
}
}
NumberPicker _buildNumberPicker() {
if (widget.decimalPlaces > 0) {
return new NumberPicker.decimal(
initialValue: selectedDoubleValue,
minValue: widget.minValue,
maxValue: widget.maxValue,
decimalPlaces: widget.decimalPlaces,
onChanged: _handleValueChanged);
} else {
return new NumberPicker.integer(
initialValue: selectedIntValue,
minValue: widget.minValue,
maxValue: widget.maxValue,
step: widget.step,
infiniteLoop: widget.infiniteLoop,
onChanged: _handleValueChanged,
);
}
}
@override
Widget build(BuildContext context) {
return new AlertDialog(
title: widget.title,
titlePadding: widget.titlePadding,
content: _buildNumberPicker(),
actions: [
new TextButton(
onPressed: () => Navigator.of(context).pop(),
child: widget.cancelWidget,
),
new TextButton(
onPressed: () => Navigator.of(context).pop(widget.decimalPlaces > 0
? selectedDoubleValue
: selectedIntValue),
child: widget.confirmWidget),
],
);
}
}

@ -1,50 +0,0 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.5"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0-nullsafety.5"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.6"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.5"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.5"
sdks:
dart: ">=2.12.0-0 <3.0.0"

@ -1,12 +0,0 @@
name: timeflow
environment:
sdk: ">=2.2.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
flutter:
uses-material-design: true
assets:
- preview.png

@ -1,11 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
<script defer src="main.dart.js" type="application/javascript"></script>
</head>
<body>
</body>
</html>
Loading…
Cancel
Save