mirror of
https://github.com/git-touch/git-touch
synced 2025-01-27 14:19:24 +01:00
feat: add error screen to handle network and other errors
This commit is contained in:
parent
ddb9497ab8
commit
0a26509cdd
@ -157,7 +157,7 @@ class _SettingsProviderState extends State<SettingsProvider> {
|
||||
} else if (Platform.isIOS) {
|
||||
theme = ThemeMap.cupertino;
|
||||
}
|
||||
theme = ThemeMap.material;
|
||||
// theme = ThemeMap.material;
|
||||
|
||||
setState(() {
|
||||
ready = true;
|
||||
@ -173,6 +173,10 @@ class _SettingsProviderState extends State<SettingsProvider> {
|
||||
});
|
||||
}
|
||||
|
||||
// http timeout
|
||||
var _timeoutDuration = Duration(seconds: 10);
|
||||
// var _timeoutDuration = Duration(seconds: 1);
|
||||
|
||||
Future<dynamic> query(String query, [String _token]) async {
|
||||
if (_token == null) {
|
||||
_token = token;
|
||||
@ -181,12 +185,15 @@ class _SettingsProviderState extends State<SettingsProvider> {
|
||||
throw Exception('token is null');
|
||||
}
|
||||
|
||||
final res = await http.post(prefix + '/graphql',
|
||||
headers: {
|
||||
HttpHeaders.authorizationHeader: 'token $_token',
|
||||
HttpHeaders.contentTypeHeader: 'application/json'
|
||||
},
|
||||
body: json.encode({'query': query}));
|
||||
final res = await http
|
||||
.post(prefix + '/graphql',
|
||||
headers: {
|
||||
HttpHeaders.authorizationHeader: 'token $_token',
|
||||
HttpHeaders.contentTypeHeader: 'application/json'
|
||||
},
|
||||
body: json.encode({'query': query}))
|
||||
.timeout(_timeoutDuration);
|
||||
|
||||
final data = json.decode(res.body);
|
||||
|
||||
if (data['errors'] != null) {
|
||||
@ -202,10 +209,9 @@ class _SettingsProviderState extends State<SettingsProvider> {
|
||||
// https://developer.github.com/v3/repos/contents/#custom-media-types
|
||||
headers[HttpHeaders.contentTypeHeader] = contentType;
|
||||
}
|
||||
final res = await http.get(
|
||||
prefix + url,
|
||||
headers: headers,
|
||||
);
|
||||
final res = await http
|
||||
.get(prefix + url, headers: headers)
|
||||
.timeout(_timeoutDuration);
|
||||
print(res.body);
|
||||
final data = json.decode(res.body);
|
||||
return data;
|
||||
@ -213,15 +219,18 @@ class _SettingsProviderState extends State<SettingsProvider> {
|
||||
|
||||
Future<dynamic> patchWithCredentials(String url) async {
|
||||
var headers = {HttpHeaders.authorizationHeader: 'token $token'};
|
||||
await http.patch(prefix + url, headers: headers);
|
||||
await http.patch(prefix + url, headers: headers).timeout(_timeoutDuration);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<dynamic> putWithCredentials(String url,
|
||||
{String contentType, String body}) async {
|
||||
var headers = {HttpHeaders.authorizationHeader: 'token $token'};
|
||||
final res =
|
||||
await http.put(prefix + url, headers: headers, body: body ?? {});
|
||||
final res = await http
|
||||
.put(prefix + url, headers: headers, body: body ?? {})
|
||||
.timeout(_timeoutDuration);
|
||||
|
||||
print(res.body);
|
||||
// final data = json.decode(res.body);
|
||||
// return data;
|
||||
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import '../providers/settings.dart';
|
||||
import '../widgets/link.dart';
|
||||
import '../widgets/error_reload.dart';
|
||||
import '../widgets/loading.dart';
|
||||
|
||||
typedef RefreshCallback = Future<void> Function();
|
||||
@ -36,6 +36,8 @@ class ListScaffold extends StatefulWidget {
|
||||
class _ListScaffoldState extends State<ListScaffold> {
|
||||
bool loading = false;
|
||||
bool loadingMore = false;
|
||||
String error = '';
|
||||
|
||||
ScrollController _controller = ScrollController();
|
||||
|
||||
@override
|
||||
@ -55,12 +57,13 @@ class _ListScaffoldState extends State<ListScaffold> {
|
||||
Future<void> _refresh() async {
|
||||
print('list scaffold refresh');
|
||||
setState(() {
|
||||
error = '';
|
||||
loading = true;
|
||||
});
|
||||
try {
|
||||
await widget.onRefresh();
|
||||
} catch (err) {
|
||||
print(err);
|
||||
error = err.toString();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@ -105,7 +108,9 @@ class _ListScaffoldState extends State<ListScaffold> {
|
||||
}
|
||||
|
||||
Widget _buildSliver(BuildContext context) {
|
||||
if (loading) {
|
||||
if (error.isNotEmpty) {
|
||||
return ErrorReload(text: error, reload: _refresh);
|
||||
} else if (loading) {
|
||||
return SliverToBoxAdapter(child: Loading(more: false));
|
||||
} else {
|
||||
return SliverList(
|
||||
@ -118,7 +123,9 @@ class _ListScaffoldState extends State<ListScaffold> {
|
||||
}
|
||||
|
||||
Widget _buildBody(BuildContext context) {
|
||||
if (loading) {
|
||||
if (error.isNotEmpty) {
|
||||
return ErrorReload(text: error, reload: _refresh);
|
||||
} else if (loading) {
|
||||
return Loading(more: false);
|
||||
} else {
|
||||
return ListView.builder(
|
||||
|
@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart';
|
||||
import '../providers/settings.dart';
|
||||
import '../widgets/loading.dart';
|
||||
import '../widgets/link.dart';
|
||||
import '../widgets/error_reload.dart';
|
||||
|
||||
class LongListPayload<T, K> {
|
||||
T header;
|
||||
@ -51,6 +52,8 @@ class LongListScaffold<T, K> extends StatefulWidget {
|
||||
class _LongListScaffoldState<T, K> extends State<LongListScaffold<T, K>> {
|
||||
bool loading;
|
||||
bool loadingMore = false;
|
||||
String error = '';
|
||||
|
||||
LongListPayload<T, K> payload;
|
||||
|
||||
@override
|
||||
@ -62,10 +65,17 @@ class _LongListScaffoldState<T, K> extends State<LongListScaffold<T, K>> {
|
||||
Future<void> _refresh() async {
|
||||
print('long list scaffold refresh');
|
||||
setState(() {
|
||||
error = '';
|
||||
loading = true;
|
||||
});
|
||||
try {
|
||||
payload = await widget.onRefresh();
|
||||
} catch (err) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
error = err.toString();
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@ -161,7 +171,9 @@ class _LongListScaffoldState<T, K> extends State<LongListScaffold<T, K>> {
|
||||
}
|
||||
|
||||
Widget _buildSliver() {
|
||||
if (loading) {
|
||||
if (error.isNotEmpty) {
|
||||
return ErrorReload(text: error, reload: _refresh);
|
||||
} else if (loading) {
|
||||
return SliverToBoxAdapter(child: Loading(more: false));
|
||||
} else {
|
||||
return SliverList(
|
||||
@ -172,7 +184,9 @@ class _LongListScaffoldState<T, K> extends State<LongListScaffold<T, K>> {
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
if (loading) {
|
||||
if (error.isNotEmpty) {
|
||||
return ErrorReload(text: error, reload: _refresh);
|
||||
} else if (loading) {
|
||||
return Loading(more: false);
|
||||
} else {
|
||||
return ListView.builder(itemCount: _itemCount, itemBuilder: _buildItem);
|
||||
|
@ -3,16 +3,14 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import '../providers/settings.dart';
|
||||
import '../widgets/loading.dart';
|
||||
|
||||
typedef RefreshCallback = Future<void> Function();
|
||||
typedef WidgetBuilder = Widget Function();
|
||||
import '../widgets/error_reload.dart';
|
||||
import '../utils/utils.dart';
|
||||
|
||||
// This is a scaffold for pull to refresh
|
||||
class RefreshScaffold extends StatelessWidget {
|
||||
class RefreshScaffold<T> extends StatefulWidget {
|
||||
final Widget title;
|
||||
final WidgetBuilder bodyBuilder;
|
||||
final RefreshCallback onRefresh;
|
||||
final bool loading;
|
||||
final Widget Function(T payload) bodyBuilder;
|
||||
final Future<T> Function() onRefresh;
|
||||
final Widget trailing;
|
||||
final List<Widget> actions;
|
||||
final PreferredSizeWidget bottom;
|
||||
@ -21,17 +19,51 @@ class RefreshScaffold extends StatelessWidget {
|
||||
@required this.title,
|
||||
@required this.bodyBuilder,
|
||||
@required this.onRefresh,
|
||||
@required this.loading,
|
||||
this.trailing,
|
||||
this.actions,
|
||||
this.bottom,
|
||||
});
|
||||
|
||||
@override
|
||||
_RefreshScaffoldState createState() => _RefreshScaffoldState();
|
||||
}
|
||||
|
||||
class _RefreshScaffoldState<T> extends State<RefreshScaffold<T>> {
|
||||
bool loading;
|
||||
T payload;
|
||||
String error = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_refresh();
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
if (loading) {
|
||||
if (error.isNotEmpty) {
|
||||
return ErrorReload(text: error, reload: _refresh);
|
||||
} else if (loading) {
|
||||
return Loading(more: true);
|
||||
} else {
|
||||
return bodyBuilder();
|
||||
return widget.bodyBuilder(payload);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
try {
|
||||
setState(() {
|
||||
error = '';
|
||||
loading = true;
|
||||
});
|
||||
payload = await widget.onRefresh();
|
||||
} catch (err) {
|
||||
error = err.toString();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,30 +72,27 @@ class RefreshScaffold extends StatelessWidget {
|
||||
switch (SettingsProvider.of(context).theme) {
|
||||
case ThemeMap.cupertino:
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar:
|
||||
CupertinoNavigationBar(middle: title, trailing: trailing),
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: widget.title, trailing: widget.trailing),
|
||||
child: SafeArea(
|
||||
child: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
CupertinoSliverRefreshControl(onRefresh: onRefresh),
|
||||
CupertinoSliverRefreshControl(onRefresh: _refresh),
|
||||
SliverToBoxAdapter(child: _buildBody())
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
default:
|
||||
return DefaultTabController(
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: title,
|
||||
actions: actions,
|
||||
bottom: bottom,
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: onRefresh,
|
||||
child: SingleChildScrollView(child: _buildBody()),
|
||||
),
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: widget.title,
|
||||
actions: widget.actions,
|
||||
bottom: widget.bottom,
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: _refresh,
|
||||
child: SingleChildScrollView(child: _buildBody()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
73
lib/scaffolds/refresh_stateless.dart
Normal file
73
lib/scaffolds/refresh_stateless.dart
Normal file
@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import '../providers/settings.dart';
|
||||
import '../widgets/loading.dart';
|
||||
import '../widgets/error_reload.dart';
|
||||
|
||||
// This is a scaffold for pull to refresh
|
||||
class RefreshStatelessScaffold extends StatelessWidget {
|
||||
final Widget title;
|
||||
final Widget Function() bodyBuilder;
|
||||
final Future<void> Function() onRefresh;
|
||||
final bool loading;
|
||||
final String error;
|
||||
final Widget trailing;
|
||||
final List<Widget> actions;
|
||||
final PreferredSizeWidget bottom;
|
||||
|
||||
RefreshStatelessScaffold({
|
||||
@required this.title,
|
||||
@required this.bodyBuilder,
|
||||
@required this.onRefresh,
|
||||
@required this.loading,
|
||||
@required this.error,
|
||||
this.trailing,
|
||||
this.actions,
|
||||
this.bottom,
|
||||
});
|
||||
|
||||
Widget _buildBody() {
|
||||
if (error.isNotEmpty) {
|
||||
return ErrorReload(text: error, reload: onRefresh);
|
||||
} else if (loading) {
|
||||
return Loading(more: true);
|
||||
} else {
|
||||
return bodyBuilder();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
switch (SettingsProvider.of(context).theme) {
|
||||
case ThemeMap.cupertino:
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar:
|
||||
CupertinoNavigationBar(middle: title, trailing: trailing),
|
||||
child: SafeArea(
|
||||
child: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
CupertinoSliverRefreshControl(onRefresh: onRefresh),
|
||||
SliverToBoxAdapter(child: _buildBody())
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
default:
|
||||
return DefaultTabController(
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: title,
|
||||
actions: actions,
|
||||
bottom: bottom,
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: onRefresh,
|
||||
child: SingleChildScrollView(child: _buildBody()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
var settings = SettingsProvider.of(context);
|
||||
|
||||
return SimpleScaffold(
|
||||
title: Text('Accounts'),
|
||||
title: Text('Select account'),
|
||||
bodyBuilder: () {
|
||||
return Container(
|
||||
child: Column(
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import '../scaffolds/refresh.dart';
|
||||
import '../scaffolds/refresh_stateless.dart';
|
||||
import '../providers/notification.dart';
|
||||
import '../providers/settings.dart';
|
||||
import '../widgets/notification_item.dart';
|
||||
@ -31,6 +31,7 @@ class NotificationScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class NotificationScreenState extends State<NotificationScreen> {
|
||||
String error = '';
|
||||
int active = 0;
|
||||
bool loading = true;
|
||||
Map<String, NotificationGroup> groupMap = {};
|
||||
@ -155,22 +156,23 @@ $key: pullRequest(number: ${item.number}) {
|
||||
}
|
||||
|
||||
Future<void> _onSwitchTab([int index]) async {
|
||||
if (index == null) {
|
||||
index = active;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
active = index;
|
||||
error = '';
|
||||
if (index != null) {
|
||||
active = index;
|
||||
}
|
||||
loading = true;
|
||||
});
|
||||
|
||||
var _groupMap = await fetchNotifications(active);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
groupMap = _groupMap;
|
||||
loading = false;
|
||||
});
|
||||
try {
|
||||
groupMap = await fetchNotifications(active);
|
||||
} catch (err) {
|
||||
error = err.toString();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -209,7 +211,7 @@ $key: pullRequest(number: ${item.number}) {
|
||||
|
||||
@override
|
||||
Widget build(context) {
|
||||
return RefreshScaffold(
|
||||
return RefreshStatelessScaffold(
|
||||
title: _buildTitle(),
|
||||
bottom: TabBar(
|
||||
onTap: _onSwitchTab,
|
||||
@ -245,6 +247,7 @@ $key: pullRequest(number: ${item.number}) {
|
||||
],
|
||||
onRefresh: _onSwitchTab,
|
||||
loading: loading,
|
||||
error: error,
|
||||
bodyBuilder: () {
|
||||
return Column(
|
||||
children: groupMap.entries
|
||||
|
@ -8,7 +8,6 @@ import '../widgets/repo_item.dart';
|
||||
import '../widgets/entry_item.dart';
|
||||
import '../screens/issues.dart';
|
||||
import '../screens/pull_requests.dart';
|
||||
import '../utils/utils.dart';
|
||||
|
||||
class RepoScreen extends StatefulWidget {
|
||||
final String owner;
|
||||
@ -21,16 +20,6 @@ class RepoScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RepoScreenState extends State<RepoScreen> {
|
||||
Map<String, dynamic> payload;
|
||||
String readme;
|
||||
bool loading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
nextTick(_refresh);
|
||||
}
|
||||
|
||||
Future queryRepo(BuildContext context) async {
|
||||
var owner = widget.owner;
|
||||
var name = widget.name;
|
||||
@ -77,30 +66,18 @@ class _RepoScreenState extends State<RepoScreen> {
|
||||
return str;
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
setState(() {
|
||||
loading = true;
|
||||
});
|
||||
List items = await Future.wait([
|
||||
queryRepo(context),
|
||||
fetchReadme(context),
|
||||
]);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
loading = false;
|
||||
payload = items[0];
|
||||
readme = items[1];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshScaffold(
|
||||
loading: loading,
|
||||
title: Text(widget.owner + '/' + widget.name),
|
||||
onRefresh: _refresh,
|
||||
bodyBuilder: () {
|
||||
onRefresh: () => Future.wait([
|
||||
queryRepo(context),
|
||||
fetchReadme(context),
|
||||
]),
|
||||
bodyBuilder: (data) {
|
||||
var payload = data[0];
|
||||
var readme = data[1];
|
||||
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
RepoItem(payload),
|
||||
|
@ -43,15 +43,6 @@ class UserScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _UserScreenState extends State<UserScreen> {
|
||||
bool loading = true;
|
||||
Map<String, dynamic> payload = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
nextTick(_refresh);
|
||||
}
|
||||
|
||||
Future queryUser(BuildContext context) async {
|
||||
var login = widget.login;
|
||||
var data = await SettingsProvider.of(context).query('''
|
||||
@ -89,7 +80,7 @@ class _UserScreenState extends State<UserScreen> {
|
||||
return data['user'];
|
||||
}
|
||||
|
||||
Widget _buildRepos() {
|
||||
Widget _buildRepos(payload) {
|
||||
String title;
|
||||
List items;
|
||||
if (payload['pinnedRepositories']['nodes'].length == 0) {
|
||||
@ -107,20 +98,7 @@ class _UserScreenState extends State<UserScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
setState(() {
|
||||
loading = true;
|
||||
});
|
||||
var _payload = await queryUser(context);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
loading = false;
|
||||
payload = _payload;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildEmail() {
|
||||
Widget _buildEmail(payload) {
|
||||
// TODO: redesign the UI to show all information
|
||||
String email = payload['email'] ?? '';
|
||||
if (email.isNotEmpty) {
|
||||
@ -172,7 +150,7 @@ class _UserScreenState extends State<UserScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshScaffold(
|
||||
onRefresh: _refresh,
|
||||
onRefresh: () => queryUser(context),
|
||||
title: Text(widget.login),
|
||||
trailing: Link(
|
||||
child: Icon(Icons.settings, size: 24),
|
||||
@ -180,8 +158,7 @@ class _UserScreenState extends State<UserScreen> {
|
||||
material: false,
|
||||
fullscreenDialog: true,
|
||||
),
|
||||
loading: loading,
|
||||
bodyBuilder: () {
|
||||
bodyBuilder: (payload) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
@ -202,7 +179,7 @@ class _UserScreenState extends State<UserScreen> {
|
||||
Text(payload['name'] ?? widget.login,
|
||||
style: TextStyle(height: 1.2)),
|
||||
Padding(padding: EdgeInsets.only(top: 10)),
|
||||
_buildEmail(),
|
||||
_buildEmail(payload),
|
||||
],
|
||||
),
|
||||
)
|
||||
@ -241,7 +218,7 @@ class _UserScreenState extends State<UserScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildRepos(),
|
||||
_buildRepos(payload),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
42
lib/widgets/error_reload.dart
Normal file
42
lib/widgets/error_reload.dart
Normal file
@ -0,0 +1,42 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'link.dart';
|
||||
|
||||
class ErrorReload extends StatelessWidget {
|
||||
final String text;
|
||||
final Function reload;
|
||||
|
||||
ErrorReload({@required this.text, @required this.reload});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 30, horizontal: 20),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Woops, something bad happened. Error message:',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
Padding(padding: EdgeInsets.only(top: 10)),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w300,
|
||||
color: Colors.redAccent,
|
||||
),
|
||||
),
|
||||
Padding(padding: EdgeInsets.only(top: 10)),
|
||||
Link(
|
||||
child: Text(
|
||||
'Reload',
|
||||
style: TextStyle(fontSize: 20, color: Colors.blueAccent),
|
||||
),
|
||||
beforeRedirect: reload,
|
||||
material: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user