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 = '''