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
class LongListScaffold<T, K> extends StatefulWidget {
final Widget title;
final List<Widget> Function(T headerPayload) actionsBuilder;
final Widget Function(T headerPayload) trailingBuilder;
final Widget Function(T headerPayload) headerBuilder;
final Widget Function(K itemPayload) itemBuilder;
@ -37,7 +36,6 @@ class LongListScaffold<T, K> extends StatefulWidget {
LongListScaffold({
@required this.title,
this.actionsBuilder,
this.trailingBuilder,
@required this.headerBuilder,
@required this.itemBuilder,
@ -217,8 +215,9 @@ class _LongListScaffoldState<T, K> extends State<LongListScaffold<T, K>> {
return Scaffold(
appBar: AppBar(
title: widget.title,
actions:
payload == null ? null : widget.actionsBuilder(payload.header),
actions: payload == null
? null
: [widget.trailingBuilder(payload.header)],
),
body: RefreshIndicator(
onRefresh: widget.onRefresh,

View File

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

View File

@ -7,7 +7,7 @@ import '../scaffolds/long_list.dart';
import '../widgets/timeline_item.dart';
import '../widgets/comment_item.dart';
import '../providers/settings.dart';
import '../widgets/link.dart';
import '../widgets/action.dart';
/// Screen for issue and pull request
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
Widget build(BuildContext context) {
return LongListScaffold(
title: Text('$owner/$name #$number'),
trailingBuilder: (payload) {
return Link(
child: Icon(Icons.more_vert, size: 24),
material: false,
beforeRedirect: () => _openActions(payload),
return ActionButton(
title: (isPullRequest ? 'Pull Request' : 'Issue') + ' Actions',
actions: [
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) {
return Column(children: <Widget>[
Container(

View File

@ -9,8 +9,7 @@ import '../scaffolds/refresh.dart';
import '../widgets/repo_item.dart';
import '../widgets/entry_item.dart';
import '../screens/issues.dart';
import '../widgets/link.dart';
import '../utils/utils.dart';
import '../widgets/action.dart';
class RepoScreen extends StatefulWidget {
final String owner;
@ -82,59 +81,42 @@ class _RepoScreenState extends State<RepoScreen> {
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
Widget build(BuildContext context) {
return RefreshScaffold(
title: Text(widget.owner + '/' + widget.name),
trailingBuilder: (payload) {
return Link(
child: Icon(Icons.more_vert, size: 24),
material: false,
beforeRedirect: () => _openActions(payload),
);
},
actionsBuilder: (payload) {
return [
Link(
iconButton: Icon(Icons.more_vert),
beforeRedirect: () => _openActions(payload),
trailingBuilder: (data) {
var payload = data[0];
return ActionButton(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']);
},
),
]);
},
onRefresh: () => Future.wait([
queryRepo(context),

View File

@ -9,6 +9,7 @@ import '../widgets/entry_item.dart';
import '../widgets/list_group.dart';
import '../widgets/repo_item.dart';
import '../widgets/link.dart';
import '../widgets/action.dart';
import '../screens/repos.dart';
import '../screens/users.dart';
import '../screens/settings.dart';
@ -135,46 +136,6 @@ class _UserScreenState extends State<UserScreen> {
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
Widget build(BuildContext context) {
return RefreshScaffold(
@ -189,30 +150,41 @@ class _UserScreenState extends State<UserScreen> {
fullscreenDialog: true,
);
} else {
return Link(
child: Icon(Icons.more_vert, size: 24),
material: false,
beforeRedirect: () => _openActions(payload),
);
}
},
actionsBuilder: (payload) {
if (widget.showSettings) {
return [
Link(
iconButton: Icon(Icons.settings),
screenBuilder: (_) => SettingsScreen(),
fullscreenDialog: true,
)
];
} else {
return [
Link(
iconButton: Icon(Icons.more_vert),
material: false,
beforeRedirect: () => _openActions(payload),
)
];
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']);
},
),
]);
return ActionButton(title: 'User Actions', actions: actions);
}
},
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) {
return TextSpan(
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();
},
);
}
}
}