refactor: refresh scaffold supports tabs
This commit is contained in:
parent
f9fcf42c98
commit
86e49d75cf
|
@ -5,20 +5,19 @@ import 'package:provider/provider.dart';
|
|||
import '../widgets/loading.dart';
|
||||
import '../widgets/error_reload.dart';
|
||||
|
||||
// This is a scaffold for pull to refresh
|
||||
class RefreshScaffold<T> extends StatefulWidget {
|
||||
final Widget title;
|
||||
final Widget Function(T payload) bodyBuilder;
|
||||
final Future<T> Function() onRefresh;
|
||||
final Future<T> Function(int activeTab) onRefresh;
|
||||
final Widget Function(T payload) trailingBuilder;
|
||||
// final List<Widget> Function(T payload) actionsBuilder;
|
||||
final List<String> tabs;
|
||||
|
||||
RefreshScaffold({
|
||||
@required this.title,
|
||||
@required this.bodyBuilder,
|
||||
@required this.onRefresh,
|
||||
this.trailingBuilder,
|
||||
// this.actionsBuilder,
|
||||
this.tabs,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -26,9 +25,10 @@ class RefreshScaffold<T> extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _RefreshScaffoldState<T> extends State<RefreshScaffold<T>> {
|
||||
bool loading;
|
||||
T payload;
|
||||
String error = '';
|
||||
bool _loading;
|
||||
T _payload;
|
||||
String _error = '';
|
||||
int _activeTab = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -37,43 +37,46 @@ class _RefreshScaffoldState<T> extends State<RefreshScaffold<T>> {
|
|||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
if (error.isNotEmpty) {
|
||||
return ErrorReload(text: error, onTap: _refresh);
|
||||
} else if (payload == null) {
|
||||
if (_error.isNotEmpty) {
|
||||
return ErrorReload(text: _error, onTap: _refresh);
|
||||
} else if (_payload == null) {
|
||||
return Loading(more: false);
|
||||
} else {
|
||||
return widget.bodyBuilder(payload);
|
||||
return widget.bodyBuilder(_payload);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
Future<void> _refresh([int activeTab]) async {
|
||||
try {
|
||||
setState(() {
|
||||
error = '';
|
||||
loading = true;
|
||||
_error = '';
|
||||
_loading = true;
|
||||
if (activeTab != null) {
|
||||
_activeTab = activeTab;
|
||||
}
|
||||
});
|
||||
payload = await widget.onRefresh();
|
||||
_payload = await widget.onRefresh(activeTab);
|
||||
} catch (err) {
|
||||
error = err.toString();
|
||||
_error = err.toString();
|
||||
throw err;
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
loading = false;
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildTrailing() {
|
||||
if (payload == null || widget.trailingBuilder == null) return null;
|
||||
if (_payload == null || widget.trailingBuilder == null) return null;
|
||||
|
||||
return widget.trailingBuilder(payload);
|
||||
return widget.trailingBuilder(_payload);
|
||||
}
|
||||
|
||||
List<Widget> _buildActions() {
|
||||
if (payload == null || widget.trailingBuilder == null) return null;
|
||||
var w = widget.trailingBuilder(payload);
|
||||
if (_payload == null || widget.trailingBuilder == null) return null;
|
||||
var w = widget.trailingBuilder(_payload);
|
||||
return [if (w != null) w];
|
||||
}
|
||||
|
||||
|
@ -83,8 +86,22 @@ class _RefreshScaffoldState<T> extends State<RefreshScaffold<T>> {
|
|||
case AppThemeType.cupertino:
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: widget.title,
|
||||
trailing: _buildTrailing(),
|
||||
middle: widget.tabs == null
|
||||
? widget.title
|
||||
: DefaultTextStyle(
|
||||
style: TextStyle(),
|
||||
child: CupertinoSegmentedControl(
|
||||
groupValue: _activeTab,
|
||||
onValueChanged: _refresh,
|
||||
children: widget.tabs.asMap().map((key, text) => MapEntry(
|
||||
key,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Text(text),
|
||||
))),
|
||||
),
|
||||
),
|
||||
trailing: widget.tabs == null ? _buildTrailing() : null,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: CustomScrollView(
|
||||
|
@ -96,16 +113,28 @@ class _RefreshScaffoldState<T> extends State<RefreshScaffold<T>> {
|
|||
),
|
||||
);
|
||||
default:
|
||||
return Scaffold(
|
||||
var w = Scaffold(
|
||||
appBar: AppBar(
|
||||
title: widget.title,
|
||||
actions: _buildActions(),
|
||||
bottom: widget.tabs == null
|
||||
? null
|
||||
: TabBar(
|
||||
onTap: _refresh,
|
||||
tabs: widget.tabs
|
||||
.map((text) => Tab(text: text.toUpperCase()))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: _refresh,
|
||||
child: SingleChildScrollView(child: _buildBody()),
|
||||
),
|
||||
);
|
||||
if (widget.tabs == null) {
|
||||
return w;
|
||||
}
|
||||
return DefaultTabController(length: widget.tabs.length, child: w);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:git_touch/models/theme.dart';
|
||||
import 'package:provider/provider.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, onTap: onRefresh);
|
||||
} else if (loading) {
|
||||
return Loading(more: false);
|
||||
} else {
|
||||
return bodyBuilder();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
switch (Provider.of<ThemeModel>(context).theme) {
|
||||
case AppThemeType.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()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:git_touch/scaffolds/refresh.dart';
|
||||
import 'package:git_touch/widgets/app_bar_title.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../scaffolds/refresh_stateless.dart';
|
||||
import 'package:git_touch/models/notification.dart';
|
||||
import 'package:git_touch/models/theme.dart';
|
||||
import 'package:git_touch/models/settings.dart';
|
||||
import '../widgets/notification_item.dart';
|
||||
import '../widgets/list_group.dart';
|
||||
|
@ -18,17 +17,6 @@ class NotificationScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
class NotificationScreenState extends State<NotificationScreen> {
|
||||
String error = '';
|
||||
int active = 0;
|
||||
bool loading = false;
|
||||
Map<String, NotificationGroup> groupMap = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
nextTick(_onSwitchTab);
|
||||
}
|
||||
|
||||
Future<Map<String, NotificationGroup>> fetchNotifications(int index) async {
|
||||
List items = await Provider.of<SettingsModel>(context).getWithCredentials(
|
||||
'/notifications?all=${index == 2}&participating=${index == 1}');
|
||||
|
@ -108,9 +96,9 @@ $key: pullRequest(number: ${item.number}) {
|
|||
}
|
||||
|
||||
Widget _buildGroupItem(
|
||||
BuildContext context,
|
||||
MapEntry<String, NotificationGroup> entry,
|
||||
) {
|
||||
BuildContext context,
|
||||
MapEntry<String, NotificationGroup> entry,
|
||||
Map<String, NotificationGroup> groupMap) {
|
||||
var group = entry.value;
|
||||
var repo = group.repo;
|
||||
return ListGroup(
|
||||
|
@ -126,7 +114,7 @@ $key: pullRequest(number: ${item.number}) {
|
|||
onTap: () async {
|
||||
await Provider.of<SettingsModel>(context)
|
||||
.putWithCredentials('/repos/$repo/notifications');
|
||||
await _onSwitchTab();
|
||||
// await _onSwitchTab(); // TODO:
|
||||
},
|
||||
child: Icon(
|
||||
Octicons.check,
|
||||
|
@ -152,75 +140,11 @@ $key: pullRequest(number: ${item.number}) {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> _onSwitchTab([int index]) async {
|
||||
if (loading) return;
|
||||
|
||||
setState(() {
|
||||
error = '';
|
||||
if (index != null) {
|
||||
active = index;
|
||||
}
|
||||
loading = true;
|
||||
});
|
||||
try {
|
||||
groupMap = await fetchNotifications(active);
|
||||
} catch (err) {
|
||||
error = err.toString();
|
||||
throw err;
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var textMap = {
|
||||
0: 'Unread',
|
||||
1: 'Paticipating',
|
||||
2: 'All',
|
||||
};
|
||||
|
||||
Widget _buildTitle() {
|
||||
switch (Provider.of<ThemeModel>(context).theme) {
|
||||
case AppThemeType.cupertino:
|
||||
// var textStyle = DefaultTextStyle.of(context).style;
|
||||
return DefaultTextStyle(
|
||||
style: TextStyle(fontSize: 16),
|
||||
child: SizedBox.expand(
|
||||
child: CupertinoSegmentedControl(
|
||||
groupValue: active,
|
||||
onValueChanged: _onSwitchTab,
|
||||
children: textMap.map((key, text) => MapEntry(key, Text(text))),
|
||||
),
|
||||
),
|
||||
);
|
||||
default:
|
||||
return AppBarTitle('Notifications');
|
||||
}
|
||||
}
|
||||
|
||||
void _confirm() async {
|
||||
var value = await Provider.of<ThemeModel>(context)
|
||||
.showConfirm(context, 'Mark all as read?');
|
||||
if (value) {
|
||||
await Provider.of<SettingsModel>(context)
|
||||
.putWithCredentials('/notifications');
|
||||
await _onSwitchTab();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(context) {
|
||||
return RefreshStatelessScaffold(
|
||||
title: _buildTitle(),
|
||||
bottom: TabBar(
|
||||
onTap: _onSwitchTab,
|
||||
tabs: textMap.entries
|
||||
.map((entry) => Tab(text: entry.value.toUpperCase()))
|
||||
.toList(),
|
||||
),
|
||||
return RefreshScaffold(
|
||||
title: AppBarTitle('Notifications'),
|
||||
tabs: ['Unread', 'Paticipating', 'All'],
|
||||
// trailing: GestureDetector(
|
||||
// child: Icon(Icons.more_vert, size: 20),
|
||||
// onTap: () async {
|
||||
|
@ -243,22 +167,27 @@ $key: pullRequest(number: ${item.number}) {
|
|||
// _onSwitchTab(value);
|
||||
// },
|
||||
// ),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.done_all),
|
||||
onPressed: _confirm,
|
||||
)
|
||||
],
|
||||
onRefresh: _onSwitchTab,
|
||||
loading: loading,
|
||||
error: error,
|
||||
bodyBuilder: () {
|
||||
trailingBuilder: (_) => IconButton(
|
||||
icon: Icon(Icons.done_all),
|
||||
onPressed: () async {
|
||||
// TODO:
|
||||
// var value = await Provider.of<ThemeModel>(context)
|
||||
// .showConfirm(context, 'Mark all as read?');
|
||||
// if (value) {
|
||||
// await Provider.of<SettingsModel>(context)
|
||||
// .putWithCredentials('/notifications');
|
||||
// await fetchNotifications(0);
|
||||
// }
|
||||
},
|
||||
),
|
||||
onRefresh: fetchNotifications,
|
||||
bodyBuilder: (groupMap) {
|
||||
return groupMap.isEmpty
|
||||
? EmptyWidget()
|
||||
: Column(children: [
|
||||
Padding(padding: EdgeInsets.only(top: 10)),
|
||||
...groupMap.entries
|
||||
.map((entry) => _buildGroupItem(context, entry))
|
||||
.map((entry) => _buildGroupItem(context, entry, groupMap))
|
||||
.toList()
|
||||
]);
|
||||
},
|
||||
|
|
|
@ -145,7 +145,7 @@ class ObjectScreen extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return RefreshScaffold(
|
||||
title: AppBarTitle(paths.join('/')),
|
||||
onRefresh: () async {
|
||||
onRefresh: (_) async {
|
||||
var data = await Provider.of<SettingsModel>(context).query('''{
|
||||
repository(owner: "$owner", name: "$name") {
|
||||
object(expression: "$_expression") {
|
||||
|
|
|
@ -55,7 +55,7 @@ class OrganizationScreen extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshScaffold(
|
||||
onRefresh: () async {
|
||||
onRefresh: (_) async {
|
||||
// Use pinnableItems instead of organization here due to token permission
|
||||
var data = await Provider.of<SettingsModel>(context).query('''
|
||||
{
|
||||
|
|
|
@ -131,7 +131,7 @@ class RepositoryScreen extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return RefreshScaffold(
|
||||
title: AppBarTitle('Repository'),
|
||||
onRefresh: () => Future.wait([
|
||||
onRefresh: (_) => Future.wait([
|
||||
queryRepo(context),
|
||||
fetchReadme(context),
|
||||
]),
|
||||
|
|
|
@ -12,38 +12,36 @@ class TrendingScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _TrendingScreenState extends State<TrendingScreen> {
|
||||
Future<List<dynamic>> _fetchTrendingRepos() async {
|
||||
var res = await http.get('https://github-trending-api.now.sh');
|
||||
var items = json.decode(res.body);
|
||||
|
||||
return items.map((item) {
|
||||
return {
|
||||
'owner': {'login': item['author'], 'avatarUrl': item['avatar']},
|
||||
'name': item['name'],
|
||||
'description': item['description'],
|
||||
'stargazers': {
|
||||
'totalCount': item['stars'],
|
||||
},
|
||||
'forks': {
|
||||
'totalCount': item['forks'],
|
||||
},
|
||||
'primaryLanguage': item['language'] == null
|
||||
? null
|
||||
: {
|
||||
'name': item['language'],
|
||||
'color': item['languageColor'],
|
||||
},
|
||||
'isPrivate': false,
|
||||
'isFork': false // TODO:
|
||||
};
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshScaffold(
|
||||
title: AppBarTitle('Trending'),
|
||||
onRefresh: _fetchTrendingRepos,
|
||||
onRefresh: (_) async {
|
||||
var res = await http.get('https://github-trending-api.now.sh');
|
||||
var items = json.decode(res.body);
|
||||
|
||||
return items.map((item) {
|
||||
return {
|
||||
'owner': {'login': item['author'], 'avatarUrl': item['avatar']},
|
||||
'name': item['name'],
|
||||
'description': item['description'],
|
||||
'stargazers': {
|
||||
'totalCount': item['stars'],
|
||||
},
|
||||
'forks': {
|
||||
'totalCount': item['forks'],
|
||||
},
|
||||
'primaryLanguage': item['language'] == null
|
||||
? null
|
||||
: {
|
||||
'name': item['language'],
|
||||
'color': item['languageColor'],
|
||||
},
|
||||
'isPrivate': false,
|
||||
'isFork': false // TODO:
|
||||
};
|
||||
}).toList();
|
||||
},
|
||||
bodyBuilder: (payload) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
|
|
|
@ -139,7 +139,7 @@ class UserScreen extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshScaffold(
|
||||
onRefresh: () {
|
||||
onRefresh: (_) {
|
||||
return Future.wait(
|
||||
[query(context), getContributions(login)],
|
||||
);
|
||||
|
|
|
@ -103,7 +103,7 @@ class TableView extends StatelessWidget {
|
|||
);
|
||||
|
||||
if (item.onTap == null && item.screenBuilder == null && item.url == null) {
|
||||
return widget;
|
||||
return Ink(color: PrimerColors.white, child: widget);
|
||||
}
|
||||
return Link(
|
||||
onTap: item.onTap,
|
||||
|
|
Loading…
Reference in New Issue