refactor: extract action button widget

This commit is contained in:
Rongjian Zhang 2019-02-20 16:31:22 +08:00
parent eac2cc5470
commit 0396ac7f7b
7 changed files with 182 additions and 207 deletions

View File

@ -28,7 +28,6 @@ class LongListPayload<T, K> {
// e.g. https://github.com/reactjs/rfcs/pull/68 // e.g. https://github.com/reactjs/rfcs/pull/68
class LongListScaffold<T, K> extends StatefulWidget { class LongListScaffold<T, K> extends StatefulWidget {
final Widget title; final Widget title;
final List<Widget> Function(T headerPayload) actionsBuilder;
final Widget Function(T headerPayload) trailingBuilder; final Widget Function(T headerPayload) trailingBuilder;
final Widget Function(T headerPayload) headerBuilder; final Widget Function(T headerPayload) headerBuilder;
final Widget Function(K itemPayload) itemBuilder; final Widget Function(K itemPayload) itemBuilder;
@ -37,7 +36,6 @@ class LongListScaffold<T, K> extends StatefulWidget {
LongListScaffold({ LongListScaffold({
@required this.title, @required this.title,
this.actionsBuilder,
this.trailingBuilder, this.trailingBuilder,
@required this.headerBuilder, @required this.headerBuilder,
@required this.itemBuilder, @required this.itemBuilder,
@ -217,8 +215,9 @@ class _LongListScaffoldState<T, K> extends State<LongListScaffold<T, K>> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: widget.title, title: widget.title,
actions: actions: payload == null
payload == null ? null : widget.actionsBuilder(payload.header), ? null
: [widget.trailingBuilder(payload.header)],
), ),
body: RefreshIndicator( body: RefreshIndicator(
onRefresh: widget.onRefresh, onRefresh: widget.onRefresh,

View File

@ -4,7 +4,6 @@ import 'package:flutter/widgets.dart';
import '../providers/settings.dart'; import '../providers/settings.dart';
import '../widgets/loading.dart'; import '../widgets/loading.dart';
import '../widgets/error_reload.dart'; import '../widgets/error_reload.dart';
import '../utils/utils.dart';
// This is a scaffold for pull to refresh // This is a scaffold for pull to refresh
class RefreshScaffold<T> extends StatefulWidget { class RefreshScaffold<T> extends StatefulWidget {
@ -12,7 +11,7 @@ class RefreshScaffold<T> extends StatefulWidget {
final Widget Function(T payload) bodyBuilder; final Widget Function(T payload) bodyBuilder;
final Future<T> Function() onRefresh; final Future<T> Function() onRefresh;
final Widget Function(T payload) trailingBuilder; final Widget Function(T payload) trailingBuilder;
final List<Widget> Function(T payload) actionsBuilder; // final List<Widget> Function(T payload) actionsBuilder;
final PreferredSizeWidget bottom; final PreferredSizeWidget bottom;
RefreshScaffold({ RefreshScaffold({
@ -20,7 +19,7 @@ class RefreshScaffold<T> extends StatefulWidget {
@required this.bodyBuilder, @required this.bodyBuilder,
@required this.onRefresh, @required this.onRefresh,
this.trailingBuilder, this.trailingBuilder,
this.actionsBuilder, // this.actionsBuilder,
this.bottom, this.bottom,
}); });
@ -67,6 +66,18 @@ class _RefreshScaffoldState<T> extends State<RefreshScaffold<T>> {
} }
} }
Widget _buildTrailing() {
if (payload == null) return null;
return widget.trailingBuilder(payload);
}
List<Widget> _buildActions() {
if (payload == null) return null;
return [widget.trailingBuilder(payload)];
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
switch (SettingsProvider.of(context).theme) { switch (SettingsProvider.of(context).theme) {
@ -74,7 +85,7 @@ class _RefreshScaffoldState<T> extends State<RefreshScaffold<T>> {
return CupertinoPageScaffold( return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar( navigationBar: CupertinoNavigationBar(
middle: widget.title, middle: widget.title,
trailing: widget.trailingBuilder(payload), trailing: _buildTrailing(),
), ),
child: SafeArea( child: SafeArea(
child: CustomScrollView( child: CustomScrollView(
@ -89,7 +100,7 @@ class _RefreshScaffoldState<T> extends State<RefreshScaffold<T>> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: widget.title, title: widget.title,
actions: widget.actionsBuilder(payload), actions: _buildActions(),
bottom: widget.bottom, bottom: widget.bottom,
), ),
body: RefreshIndicator( body: RefreshIndicator(

View File

@ -7,7 +7,7 @@ import '../scaffolds/long_list.dart';
import '../widgets/timeline_item.dart'; import '../widgets/timeline_item.dart';
import '../widgets/comment_item.dart'; import '../widgets/comment_item.dart';
import '../providers/settings.dart'; import '../providers/settings.dart';
import '../widgets/link.dart'; import '../widgets/action.dart';
/// Screen for issue and pull request /// Screen for issue and pull request
class IssueScreen extends StatefulWidget { class IssueScreen extends StatefulWidget {
@ -333,48 +333,29 @@ __typename
); );
} }
Future<void> _openActions(payload) async {
if (payload == null) return;
showActions(
context,
title: (isPullRequest ? 'Pull Request' : 'Issue') + ' Actions',
actions: [
Action(
text: 'Share',
onPress: () {
Share.share(payload['url']);
},
),
Action(
text: 'Open in Browser',
onPress: () {
launch(payload['url']);
},
),
],
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LongListScaffold( return LongListScaffold(
title: Text('$owner/$name #$number'), title: Text('$owner/$name #$number'),
trailingBuilder: (payload) { trailingBuilder: (payload) {
return Link( return ActionButton(
child: Icon(Icons.more_vert, size: 24), title: (isPullRequest ? 'Pull Request' : 'Issue') + ' Actions',
material: false, actions: [
beforeRedirect: () => _openActions(payload), Action(
text: 'Share',
onPress: () {
Share.share(payload['url']);
},
),
Action(
text: 'Open in Browser',
onPress: () {
launch(payload['url']);
},
),
],
); );
}, },
actionsBuilder: (payload) {
return [
Link(
iconButton: Icon(Icons.more_vert),
beforeRedirect: () => _openActions(payload),
),
];
},
headerBuilder: (payload) { headerBuilder: (payload) {
return Column(children: <Widget>[ return Column(children: <Widget>[
Container( Container(

View File

@ -9,8 +9,7 @@ import '../scaffolds/refresh.dart';
import '../widgets/repo_item.dart'; import '../widgets/repo_item.dart';
import '../widgets/entry_item.dart'; import '../widgets/entry_item.dart';
import '../screens/issues.dart'; import '../screens/issues.dart';
import '../widgets/link.dart'; import '../widgets/action.dart';
import '../utils/utils.dart';
class RepoScreen extends StatefulWidget { class RepoScreen extends StatefulWidget {
final String owner; final String owner;
@ -82,59 +81,42 @@ class _RepoScreenState extends State<RepoScreen> {
return str; return str;
} }
Future<void> _openActions(data) async {
if (data == null) return;
var payload = data[0];
showActions(context, title: 'Repository Actions', actions: [
Action(
text: payload['viewerHasStarred'] ? 'Unstar' : 'Star',
onPress: () async {
if (payload['viewerHasStarred']) {
await SettingsProvider.of(context)
.deleteWithCredentials('/user/starred/$owner/$name');
payload['viewerHasStarred'] = false;
} else {
SettingsProvider.of(context)
.putWithCredentials('/user/starred/$owner/$name');
payload['viewerHasStarred'] = true;
}
},
),
// TODO: watch
Action(
text: 'Share',
onPress: () {
Share.share(payload['url']);
},
),
Action(
text: 'Open in Browser',
onPress: () {
launch(payload['url']);
},
),
]);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RefreshScaffold( return RefreshScaffold(
title: Text(widget.owner + '/' + widget.name), title: Text(widget.owner + '/' + widget.name),
trailingBuilder: (payload) { trailingBuilder: (data) {
return Link( var payload = data[0];
child: Icon(Icons.more_vert, size: 24),
material: false, return ActionButton(title: 'Repository Actions', actions: [
beforeRedirect: () => _openActions(payload), Action(
); text: payload['viewerHasStarred'] ? 'Unstar' : 'Star',
}, onPress: () async {
actionsBuilder: (payload) { if (payload['viewerHasStarred']) {
return [ await SettingsProvider.of(context)
Link( .deleteWithCredentials('/user/starred/$owner/$name');
iconButton: Icon(Icons.more_vert), payload['viewerHasStarred'] = false;
beforeRedirect: () => _openActions(payload), } else {
SettingsProvider.of(context)
.putWithCredentials('/user/starred/$owner/$name');
payload['viewerHasStarred'] = true;
}
},
), ),
]; // TODO: watch
Action(
text: 'Share',
onPress: () {
Share.share(payload['url']);
},
),
Action(
text: 'Open in Browser',
onPress: () {
launch(payload['url']);
},
),
]);
}, },
onRefresh: () => Future.wait([ onRefresh: () => Future.wait([
queryRepo(context), queryRepo(context),

View File

@ -9,6 +9,7 @@ import '../widgets/entry_item.dart';
import '../widgets/list_group.dart'; import '../widgets/list_group.dart';
import '../widgets/repo_item.dart'; import '../widgets/repo_item.dart';
import '../widgets/link.dart'; import '../widgets/link.dart';
import '../widgets/action.dart';
import '../screens/repos.dart'; import '../screens/repos.dart';
import '../screens/users.dart'; import '../screens/users.dart';
import '../screens/settings.dart'; import '../screens/settings.dart';
@ -135,46 +136,6 @@ class _UserScreenState extends State<UserScreen> {
return Container(); return Container();
} }
Future<void> _openActions(payload) async {
if (payload == null) return;
List<Action> actions = [];
if (payload['viewerCanFollow']) {
actions.add(Action(
text: payload['viewerIsFollowing'] ? 'Unfollow' : 'Follow',
onPress: () async {
if (payload['viewerIsFollowing']) {
await SettingsProvider.of(context)
.deleteWithCredentials('/user/following/${widget.login}');
payload['viewerIsFollowing'] = false;
} else {
SettingsProvider.of(context)
.putWithCredentials('/user/following/${widget.login}');
payload['viewerIsFollowing'] = true;
}
},
));
}
actions.addAll([
Action(
text: 'Share',
onPress: () {
Share.share(payload['url']);
},
),
Action(
text: 'Open in Browser',
onPress: () {
launch(payload['url']);
},
),
]);
showActions(context, title: 'User Actions', actions: actions);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RefreshScaffold( return RefreshScaffold(
@ -189,30 +150,41 @@ class _UserScreenState extends State<UserScreen> {
fullscreenDialog: true, fullscreenDialog: true,
); );
} else { } else {
return Link( List<Action> actions = [];
child: Icon(Icons.more_vert, size: 24),
material: false, if (payload['viewerCanFollow']) {
beforeRedirect: () => _openActions(payload), actions.add(Action(
); text: payload['viewerIsFollowing'] ? 'Unfollow' : 'Follow',
} onPress: () async {
}, if (payload['viewerIsFollowing']) {
actionsBuilder: (payload) { await SettingsProvider.of(context)
if (widget.showSettings) { .deleteWithCredentials('/user/following/${widget.login}');
return [ payload['viewerIsFollowing'] = false;
Link( } else {
iconButton: Icon(Icons.settings), SettingsProvider.of(context)
screenBuilder: (_) => SettingsScreen(), .putWithCredentials('/user/following/${widget.login}');
fullscreenDialog: true, payload['viewerIsFollowing'] = true;
) }
]; },
} else { ));
return [ }
Link(
iconButton: Icon(Icons.more_vert), actions.addAll([
material: false, Action(
beforeRedirect: () => _openActions(payload), text: 'Share',
) onPress: () {
]; Share.share(payload['url']);
},
),
Action(
text: 'Open in Browser',
onPress: () {
launch(payload['url']);
},
),
]);
return ActionButton(title: 'User Actions', actions: actions);
} }
}, },
bodyBuilder: (payload) { bodyBuilder: (payload) {

View File

@ -118,54 +118,6 @@ Future<T> showOptions<T>(BuildContext context, List<DialogOption<T>> options) {
} }
} }
class Action {
String text;
Function onPress;
Action({@required this.text, @required this.onPress});
}
Future showActions(
BuildContext context, {
@required String title,
@required List<Action> actions,
}) async {
int result;
switch (SettingsProvider.of(context).theme) {
case ThemeMap.cupertino:
result = await showCupertinoModalPopup<int>(
context: context,
builder: (BuildContext context) {
return CupertinoActionSheet(
title: Text(title),
actions: actions.asMap().entries.map((entry) {
return CupertinoActionSheetAction(
child: Text(entry.value.text),
onPressed: () {
Navigator.pop(context, entry.key);
},
);
}).toList(),
cancelButton: CupertinoActionSheetAction(
child: const Text('Cancel'),
isDefaultAction: true,
onPressed: () {
Navigator.pop(context);
},
),
);
},
);
break;
default:
}
if (result != null) {
actions[result].onPress();
}
}
TextSpan createLinkSpan(BuildContext context, String text, Function handle) { TextSpan createLinkSpan(BuildContext context, String text, Function handle) {
return TextSpan( return TextSpan(
text: text, text: text,

78
lib/widgets/action.dart Normal file
View File

@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import '../providers/settings.dart';
class Action {
String text;
Function onPress;
Action({@required this.text, @required this.onPress});
}
class ActionButton extends StatelessWidget {
final String title;
final List<Action> actions;
final IconData iconData;
ActionButton({
@required this.title,
@required this.actions,
this.iconData = Icons.more_vert,
});
void _onSelected(int value) {
if (value != null) {
actions[value].onPress();
}
}
@override
Widget build(BuildContext context) {
switch (SettingsProvider.of(context).theme) {
case ThemeMap.cupertino:
return GestureDetector(
child: Icon(iconData, size: 24),
onTap: () async {
int value = await showCupertinoModalPopup<int>(
context: context,
builder: (BuildContext context) {
return CupertinoActionSheet(
title: Text(title),
actions: actions.asMap().entries.map((entry) {
return CupertinoActionSheetAction(
child: Text(entry.value.text),
onPressed: () {
Navigator.pop(context, entry.key);
},
);
}).toList(),
cancelButton: CupertinoActionSheetAction(
child: const Text('Cancel'),
isDefaultAction: true,
onPressed: () {
Navigator.pop(context);
},
),
);
},
);
_onSelected(value);
},
);
default:
return PopupMenuButton<int>(
icon: Icon(iconData),
onSelected: _onSelected,
itemBuilder: (BuildContext context) {
return actions.asMap().entries.map((entry) {
return PopupMenuItem(
value: entry.key,
child: Text(entry.value.text),
);
}).toList();
},
);
}
}
}