From d9bec759e3bcc2715878c29f907520ac5c19da8b Mon Sep 17 00:00:00 2001 From: Rongjian Zhang <pd4d10@gmail.com> Date: Sun, 10 Feb 2019 18:50:40 +0800 Subject: [PATCH] feat: add actions to repo, user and issue screen --- lib/providers/settings.dart | 12 +++- lib/scaffolds/long_list.dart | 21 ++++--- lib/scaffolds/refresh.dart | 14 +++-- lib/screens/issue.dart | 61 +++++++++++++++++++ lib/screens/pull_request.dart | 61 +++++++++++++++++++ lib/screens/repo.dart | 85 +++++++++++++++++++++++++- lib/screens/user.dart | 111 ++++++++++++++++++++++++++++++---- lib/utils/utils.dart | 1 + 8 files changed, 337 insertions(+), 29 deletions(-) diff --git a/lib/providers/settings.dart b/lib/providers/settings.dart index 46db739..d96359e 100644 --- a/lib/providers/settings.dart +++ b/lib/providers/settings.dart @@ -221,6 +221,7 @@ class _SettingsProviderState extends State<SettingsProvider> { .timeout(_timeoutDuration); final data = json.decode(res.body); + print(data); if (data['errors'] != null) { throw new Exception(data['errors'].toString()); @@ -257,12 +258,21 @@ class _SettingsProviderState extends State<SettingsProvider> { .put(prefix + url, headers: headers, body: body ?? {}) .timeout(_timeoutDuration); - // print(res.body); + print(res.body); // final data = json.decode(res.body); // return data; return true; } + Future<dynamic> deleteWithCredentials(String url) async { + var headers = {HttpHeaders.authorizationHeader: 'token $token'}; + final res = await http + .delete(prefix + url, headers: headers) + .timeout(_timeoutDuration); + print(res.body); + return true; + } + String randomString; generateRandomString() { diff --git a/lib/scaffolds/long_list.dart b/lib/scaffolds/long_list.dart index a4cfa39..42d224a 100644 --- a/lib/scaffolds/long_list.dart +++ b/lib/scaffolds/long_list.dart @@ -28,8 +28,8 @@ 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> actions; - final Widget trailing; + 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; final Future<LongListPayload<T, K>> Function() onRefresh; @@ -37,8 +37,8 @@ class LongListScaffold<T, K> extends StatefulWidget { LongListScaffold({ @required this.title, - this.actions, - this.trailing, + this.actionsBuilder, + this.trailingBuilder, @required this.headerBuilder, @required this.itemBuilder, @required this.onRefresh, @@ -208,21 +208,28 @@ class _LongListScaffoldState<T, K> extends State<LongListScaffold<T, K>> { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( middle: widget.title, - trailing: widget.trailing, + trailing: + payload == null ? null : widget.trailingBuilder(payload.header), ), child: SafeArea( child: CustomScrollView(slivers: slivers), ), ); default: + List<Widget> children = []; + if (payload != null) { + children.add(widget.headerBuilder(payload.header)); + } + children.add(_buildBody()); return Scaffold( appBar: AppBar( title: widget.title, - actions: widget.actions, + actions: + payload == null ? null : widget.actionsBuilder(payload.header), ), body: RefreshIndicator( onRefresh: widget.onRefresh, - child: _buildBody(), + child: Column(children: children), ), ); } diff --git a/lib/scaffolds/refresh.dart b/lib/scaffolds/refresh.dart index 5f7a043..362f865 100644 --- a/lib/scaffolds/refresh.dart +++ b/lib/scaffolds/refresh.dart @@ -11,16 +11,16 @@ class RefreshScaffold<T> extends StatefulWidget { final Widget title; final Widget Function(T payload) bodyBuilder; final Future<T> Function() onRefresh; - final Widget trailing; - final List<Widget> actions; + final Widget Function(T payload) trailingBuilder; + final List<Widget> Function(T payload) actionsBuilder; final PreferredSizeWidget bottom; RefreshScaffold({ @required this.title, @required this.bodyBuilder, @required this.onRefresh, - this.trailing, - this.actions, + this.trailingBuilder, + this.actionsBuilder, this.bottom, }); @@ -73,7 +73,9 @@ class _RefreshScaffoldState<T> extends State<RefreshScaffold<T>> { case ThemeMap.cupertino: return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( - middle: widget.title, trailing: widget.trailing), + middle: widget.title, + trailing: widget.trailingBuilder(payload), + ), child: SafeArea( child: CustomScrollView( slivers: <Widget>[ @@ -87,7 +89,7 @@ class _RefreshScaffoldState<T> extends State<RefreshScaffold<T>> { return Scaffold( appBar: AppBar( title: widget.title, - actions: widget.actions, + actions: widget.actionsBuilder(payload), bottom: widget.bottom, ), body: RefreshIndicator( diff --git a/lib/screens/issue.dart b/lib/screens/issue.dart index 76dd9e9..d8bfa7c 100644 --- a/lib/screens/issue.dart +++ b/lib/screens/issue.dart @@ -1,10 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; +import 'package:share/share.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../utils/utils.dart'; import '../scaffolds/long_list.dart'; import '../widgets/timeline_item.dart'; import '../widgets/comment_item.dart'; import '../providers/settings.dart'; +import '../widgets/link.dart'; class IssueScreen extends StatefulWidget { final int number; @@ -130,10 +133,68 @@ class _IssueScreenState extends State<IssueScreen> { ); } + Future<void> _openActions(payload) async { + if (payload == null) return; + + var _actionMap = { + 2: 'Share', + 3: 'Open in Browser', + }; + + var value = await showCupertinoModalPopup<int>( + context: context, + builder: (BuildContext context) { + return CupertinoActionSheet( + title: Text('Issue Actions'), + actions: _actionMap.entries.map((entry) { + return CupertinoActionSheetAction( + child: Text(entry.value), + onPressed: () { + Navigator.pop(context, entry.key); + }, + ); + }).toList(), + cancelButton: CupertinoActionSheetAction( + child: const Text('Cancel'), + isDefaultAction: true, + onPressed: () { + Navigator.pop(context); + }, + ), + ); + }, + ); + + switch (value) { + case 2: + Share.share(payload['url']); + break; + case 3: + launch(payload['url']); + break; + default: + } + } + @override Widget build(BuildContext context) { return LongListScaffold( title: Text(_fullName + ' #' + widget.number.toString()), + 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), + ), + ]; + }, headerBuilder: (payload) { return Column(children: <Widget>[ Container( diff --git a/lib/screens/pull_request.dart b/lib/screens/pull_request.dart index b7d4249..b62f2af 100644 --- a/lib/screens/pull_request.dart +++ b/lib/screens/pull_request.dart @@ -1,10 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; +import 'package:share/share.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../providers/settings.dart'; import '../utils/utils.dart'; import '../scaffolds/long_list.dart'; import '../widgets/timeline_item.dart'; import '../widgets/comment_item.dart'; +import '../widgets/link.dart'; class PullRequestScreen extends StatefulWidget { final int number; @@ -181,10 +184,68 @@ class _PullRequestScreenState extends State<PullRequestScreen> { get _fullName => widget.owner + '/' + widget.name; + Future<void> _openActions(payload) async { + if (payload == null) return; + + var _actionMap = { + 2: 'Share', + 3: 'Open in Browser', + }; + + var value = await showCupertinoModalPopup<int>( + context: context, + builder: (BuildContext context) { + return CupertinoActionSheet( + title: Text('Issue Actions'), + actions: _actionMap.entries.map((entry) { + return CupertinoActionSheetAction( + child: Text(entry.value), + onPressed: () { + Navigator.pop(context, entry.key); + }, + ); + }).toList(), + cancelButton: CupertinoActionSheetAction( + child: const Text('Cancel'), + isDefaultAction: true, + onPressed: () { + Navigator.pop(context); + }, + ), + ); + }, + ); + + switch (value) { + case 2: + Share.share(payload['url']); + break; + case 3: + launch(payload['url']); + break; + default: + } + } + @override Widget build(BuildContext context) { return LongListScaffold( title: Text(_fullName + ' #' + widget.number.toString()), + 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), + ), + ]; + }, headerBuilder: (payload) { return Column(children: <Widget>[ Container( diff --git a/lib/screens/repo.dart b/lib/screens/repo.dart index c3930cb..79af3f9 100644 --- a/lib/screens/repo.dart +++ b/lib/screens/repo.dart @@ -2,11 +2,14 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:share/share.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../providers/settings.dart'; import '../scaffolds/refresh.dart'; import '../widgets/repo_item.dart'; import '../widgets/entry_item.dart'; import '../screens/issues.dart'; +import '../widgets/link.dart'; class RepoScreen extends StatefulWidget { final String owner; @@ -19,12 +22,14 @@ class RepoScreen extends StatefulWidget { } class _RepoScreenState extends State<RepoScreen> { + get owner => widget.owner; + get name => widget.name; + Future queryRepo(BuildContext context) async { - var owner = widget.owner; - var name = widget.name; var data = await SettingsProvider.of(context).query(''' { repository(owner: "$owner", name: "$name") { + id owner { login } @@ -52,6 +57,8 @@ class _RepoScreenState extends State<RepoScreen> { defaultBranchRef { name } + viewerHasStarred + viewerSubscription } } @@ -69,10 +76,84 @@ class _RepoScreenState extends State<RepoScreen> { return str; } + Future<void> _openActions(data) async { + if (data == null) return; + var payload = data[0]; + + var _actionMap = { + 0: payload['viewerHasStarred'] ? 'Unstar' : 'Star', + // 1: 'Watch', TODO: + 2: 'Share', + 3: 'Open in Browser', + }; + + var value = await showCupertinoModalPopup<int>( + context: context, + builder: (BuildContext context) { + return CupertinoActionSheet( + title: Text('Repository Actions'), + actions: _actionMap.entries.map((entry) { + return CupertinoActionSheetAction( + child: Text(entry.value), + onPressed: () { + Navigator.pop(context, entry.key); + }, + ); + }).toList(), + cancelButton: CupertinoActionSheetAction( + child: const Text('Cancel'), + isDefaultAction: true, + onPressed: () { + Navigator.pop(context); + }, + ), + ); + }, + ); + + switch (value) { + case 0: + 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; + } + break; + // case 1: + // break; + case 2: + Share.share(payload['url']); + break; + case 3: + launch(payload['url']); + break; + default: + } + } + @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), + ), + ]; + }, onRefresh: () => Future.wait([ queryRepo(context), fetchReadme(context), diff --git a/lib/screens/user.dart b/lib/screens/user.dart index 2debb5c..97bb8a4 100644 --- a/lib/screens/user.dart +++ b/lib/screens/user.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:share/share.dart'; import '../providers/settings.dart'; import '../scaffolds/refresh.dart'; import '../widgets/avatar.dart'; @@ -54,6 +55,9 @@ class _UserScreenState extends State<UserScreen> { $repoChunk } } + viewerCanFollow + viewerIsFollowing + url } } '''); @@ -83,6 +87,7 @@ class _UserScreenState extends State<UserScreen> { String email = payload['email'] ?? ''; if (email.isNotEmpty) { return Link( + material: false, child: Row(children: <Widget>[ Icon( Octicons.mail, @@ -127,24 +132,104 @@ class _UserScreenState extends State<UserScreen> { return Container(); } + Future<void> _openActions(payload) async { + if (payload == null) return; + + var _actionMap = {}; + if (payload['viewerCanFollow']) { + _actionMap[0] = payload['viewerIsFollowing'] ? 'Unfollow' : 'Follow'; + } + _actionMap[2] = 'Share'; + _actionMap[3] = 'Open in Browser'; + + var value = await showCupertinoModalPopup<int>( + context: context, + builder: (BuildContext context) { + return CupertinoActionSheet( + title: Text('User Actions'), + actions: _actionMap.entries.map((entry) { + return CupertinoActionSheetAction( + child: Text(entry.value), + onPressed: () { + Navigator.pop(context, entry.key); + }, + ); + }).toList(), + cancelButton: CupertinoActionSheetAction( + child: const Text('Cancel'), + isDefaultAction: true, + onPressed: () { + Navigator.pop(context); + }, + ), + ); + }, + ); + + switch (value) { + case 0: + 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; + } + break; + // case 1: + // break; + case 2: + Share.share(payload['url']); + break; + case 3: + launch(payload['url']); + break; + default: + } + } + @override Widget build(BuildContext context) { return RefreshScaffold( onRefresh: () => queryUser(context), title: Text(widget.login), - trailing: Link( - child: Icon(Icons.settings, size: 24), - screenBuilder: (_) => SettingsScreen(), - material: false, - fullscreenDialog: true, - ), - actions: <Widget>[ - Link( - iconButton: Icon(Icons.settings), - screenBuilder: (_) => SettingsScreen(), - fullscreenDialog: true, - ), - ], + trailingBuilder: (payload) { + if (widget.showSettings) { + return Link( + child: Icon(Icons.settings, size: 24), + screenBuilder: (_) => SettingsScreen(), + material: false, + 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), + ) + ]; + } + }, bodyBuilder: (payload) { return Column( children: <Widget>[ diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index b528a31..6085051 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -161,6 +161,7 @@ author { avatarUrl } closed +url '''; var graghqlChunk = '''