From 45687a48a6c3b226a95ecd11f26cf97de9b70711 Mon Sep 17 00:00:00 2001 From: Rongjian Zhang Date: Sun, 22 Sep 2019 01:02:14 +0800 Subject: [PATCH] feat: organization screen --- lib/models/settings.dart | 3 +- lib/screens/organization.dart | 234 ++++++++++++---------------------- lib/screens/repo.dart | 12 +- lib/screens/repos.dart | 27 ++-- lib/screens/user.dart | 45 +++---- 5 files changed, 117 insertions(+), 204 deletions(-) diff --git a/lib/models/settings.dart b/lib/models/settings.dart index b87e517..cd82f76 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -303,8 +303,9 @@ class SettingsModel with ChangeNotifier { String _oauthState; void redirectToGithubOauth() { _oauthState = nanoid(); + var scope = Uri.encodeComponent('user,repo,read:org'); launch( - 'https://github.com/login/oauth/authorize?client_id=$clientId&redirect_uri=gittouch://login&scope=user%20repo&state=$_oauthState', + 'https://github.com/login/oauth/authorize?client_id=$clientId&redirect_uri=gittouch://login&scope=$scope&state=$_oauthState', ); } } diff --git a/lib/screens/organization.dart b/lib/screens/organization.dart index 8a4ae9f..8816d83 100644 --- a/lib/screens/organization.dart +++ b/lib/screens/organization.dart @@ -1,44 +1,63 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:git_touch/widgets/app_bar_title.dart'; +import 'package:git_touch/widgets/table_view.dart'; +import 'package:git_touch/widgets/user_item.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:share/share.dart'; import 'package:git_touch/models/settings.dart'; import 'package:provider/provider.dart'; import '../scaffolds/refresh.dart'; -import '../widgets/avatar.dart'; -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 '../utils/utils.dart'; -class OrganizationScreen extends StatefulWidget { +class OrganizationScreen extends StatelessWidget { final String login; - OrganizationScreen(this.login); - _OrganizationScreenState createState() => _OrganizationScreenState(); -} -class _OrganizationScreenState extends State { - Future query() async { - var login = widget.login; - var data = await Provider.of(context).query(''' + OrganizationScreen(this.login); + + Iterable _buildRepos(payload) { + String title; + List items; + + if ((payload['pinnedItems']['nodes'] as List).isNotEmpty) { + title = 'pinned repositories'; + items = payload['pinnedItems']['nodes'] as List; + } else { + items = []; + } + + items = items + .where((x) => x != null) + .toList(); // TODO: Pinned items may include somethings other than repo + if (items.isEmpty) return []; + + return [ + borderView1, + if (title != null) TableViewHeader(title), + ...join( + borderView, + items.map((item) { + return RepoItem(item); + }).toList(), + ) + ]; + } + + @override + Widget build(BuildContext context) { + return RefreshScaffold( + onRefresh: () async { + var data = await Provider.of(context).query(''' { organization(login: "$login") { name avatarUrl - websiteUrl - email + description location - repositories(first: $pageSize, orderBy: {field: PUSHED_AT, direction: DESC}) { - totalCount - nodes { - $repoChunk - } - } + email + websiteUrl pinnedItems(first: $pageSize) { nodes { ... on Repository { @@ -47,95 +66,14 @@ class _OrganizationScreenState extends State { } } url - membersWithRole { - totalCount - } } } '''); - return data['organization']; - } - - Widget _buildRepos(payload) { - String title; - List items; - if (payload['pinnedItems']['nodes'].length == 0) { - title = 'Popular repositories'; - items = payload['repositories']['nodes']; - } else { - title = 'Pinned repositories'; - items = payload['pinnedItems']['nodes']; - } - - return ListGroup( - title: Text( - title, - style: TextStyle(fontSize: 16), - ), - items: items, - itemBuilder: (item, _) { - return RepoItem(item); + return data['organization']; }, - ); - } - - Widget _buildInfo(payload) { - // TODO: redesign the UI to show all information - String email = payload['email'] ?? ''; - if (email.isNotEmpty) { - return Link( - material: false, - child: Row(children: [ - Icon( - Octicons.mail, - color: Colors.black54, - size: 16, - ), - Padding(padding: EdgeInsets.only(left: 4)), - Text(email, style: TextStyle(color: Colors.black54, fontSize: 15)) - ]), - onTap: () { - launch('mailto:' + email); - }, - ); - } - - String website = payload['websiteUrl'] ?? ''; - if (website.isNotEmpty) { - return Row(children: [ - Icon( - Octicons.link, - color: Colors.black54, - size: 16, - ), - Padding(padding: EdgeInsets.only(left: 4)), - Text(website, style: TextStyle(color: Colors.black54, fontSize: 16)) - ]); - } - - String location = payload['location'] ?? ''; - if (location.isNotEmpty) { - return Row(children: [ - Icon( - Octicons.location, - color: Colors.black54, - size: 16, - ), - Padding(padding: EdgeInsets.only(left: 4)), - Text(location, style: TextStyle(color: Colors.black54, fontSize: 16)) - ]); - } - - return Container(); - } - - @override - Widget build(BuildContext context) { - return RefreshScaffold( - onRefresh: query, - title: AppBarTitle(widget.login), + title: AppBarTitle('Organization'), trailingBuilder: (payload) { - return ActionButton(title: 'User Actions', actions: [ + return ActionButton(title: 'Organization Actions', actions: [ MyAction( text: 'Share', onPress: () { @@ -152,55 +90,51 @@ class _OrganizationScreenState extends State { }, bodyBuilder: (payload) { return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Container( - padding: EdgeInsets.all(10), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Avatar(url: payload['avatarUrl'], size: 28), - Padding(padding: EdgeInsets.only(left: 10)), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - payload['name'] ?? widget.login, - style: TextStyle(height: 1.2, fontSize: 16), - ), - Padding(padding: EdgeInsets.only(top: 10)), - _buildInfo(payload), - ], - ), - ) - ], - ), + UserItem( + login, + name: payload['name'], + avatarUrl: payload['avatarUrl'], + bio: payload['description'], ), - Container( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: Colors.black12), - top: BorderSide(color: Colors.black12), - ), - ), - child: Row( - children: [ - EntryItem( - count: payload['repositories']['totalCount'], - text: 'Repositories', - screenBuilder: (_) => - ReposScreen(login: widget.login, org: true), + borderView1, + TableView( + hasIcon: true, + items: [ + if (isNotNullOrEmpty(payload['location'])) + TableViewItem( + leftIconData: Octicons.location, + text: Text(payload['location']), + onTap: () { + launch('https://www.google.com/maps/place/' + + (payload['location'] as String) + .replaceAll(RegExp(r'\s+'), '')); + }, ), - EntryItem( - count: payload['membersWithRole']['totalCount'], - text: 'Members', - screenBuilder: (_) => UsersScreen( - login: widget.login, type: UsersScreenType.orgs), + if (isNotNullOrEmpty(payload['email'])) + TableViewItem( + leftIconData: Octicons.mail, + text: Text(payload['email']), + onTap: () { + launch('mailto:' + payload['email']); + }, ), - ], - ), + if (isNotNullOrEmpty(payload['websiteUrl'])) + TableViewItem( + leftIconData: Octicons.link, + text: Text(payload['websiteUrl']), + onTap: () { + var url = payload['websiteUrl'] as String; + if (!url.startsWith('http')) { + url = 'http://$url'; + } + launch(url); + }, + ), + ], ), - _buildRepos(payload), + ..._buildRepos(payload), ], ); }, diff --git a/lib/screens/repo.dart b/lib/screens/repo.dart index 697d395..c61145f 100644 --- a/lib/screens/repo.dart +++ b/lib/screens/repo.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'package:filesize/filesize.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; @@ -8,7 +7,6 @@ import 'package:git_touch/utils/utils.dart'; import 'package:git_touch/widgets/app_bar_title.dart'; import 'package:git_touch/widgets/markdown_view.dart'; import 'package:git_touch/widgets/table_view.dart'; -import 'package:primer/primer.dart'; import 'package:provider/provider.dart'; import 'package:git_touch/models/theme.dart'; import 'package:git_touch/screens/commits.dart'; @@ -42,7 +40,6 @@ class RepoScreen extends StatelessWidget { owner { __typename login - url avatarUrl } name @@ -120,13 +117,8 @@ class RepoScreen extends StatelessWidget { switch (payload['owner']['__typename']) { case 'Organization': - // builder = (_) => OrganizationScreen(owner); - // break; - - // Seems organization permission is a little complicated - // So we just launch browser currently - launch(payload['owner']['url']); - return; + builder = (_) => OrganizationScreen(owner); + break; case 'User': builder = (_) => UserScreen(owner); break; diff --git a/lib/screens/repos.dart b/lib/screens/repos.dart index be23236..2fde36b 100644 --- a/lib/screens/repos.dart +++ b/lib/screens/repos.dart @@ -7,33 +7,28 @@ import '../utils/utils.dart'; import '../widgets/repo_item.dart'; /// Repos of user -class ReposScreen extends StatefulWidget { +class ReposScreen extends StatelessWidget { final String login; final bool star; final bool org; - ReposScreen({this.login, this.star = false, this.org = false}); + ReposScreen(this.login, {this.star = false, this.org = false}); - @override - _ReposScreenState createState() => _ReposScreenState(); -} - -class _ReposScreenState extends State { - String get login => widget.login; - String get scope => widget.org ? 'organization' : 'user'; - String get resource => widget.star ? 'starredRepositories' : 'repositories'; + String get scope => org ? 'organization' : 'user'; + String get resource => star ? 'starredRepositories' : 'repositories'; String get fieldOrderBy { - if (widget.star) { + if (star) { return 'STARRED_AT'; } - if (widget.org) { + if (org) { return 'PUSHED_AT'; } return 'UPDATED_AT'; } - Future _queryRepos([String cursor]) async { + Future _queryRepos(BuildContext context, [String cursor]) async { var cursorChunk = cursor == null ? '' : ', after: "$cursor"'; + // FIXME: organization scope not work due to permissions var data = await Provider.of(context).query(''' { $scope(login: "$login") { @@ -61,9 +56,9 @@ class _ReposScreenState extends State { @override Widget build(BuildContext context) { return ListScaffold( - title: AppBarTitle(widget.star ? 'Stars' : 'Repositories'), - onRefresh: () => _queryRepos(), - onLoadMore: (cursor) => _queryRepos(cursor), + title: AppBarTitle(star ? 'Stars' : 'Repositories'), + onRefresh: () => _queryRepos(context), + onLoadMore: (cursor) => _queryRepos(context, cursor), itemBuilder: (payload) => RepoItem(payload), ); } diff --git a/lib/screens/user.dart b/lib/screens/user.dart index 6a64bf6..794cc32 100644 --- a/lib/screens/user.dart +++ b/lib/screens/user.dart @@ -3,7 +3,6 @@ import 'package:flutter/cupertino.dart'; import 'package:git_touch/widgets/app_bar_title.dart'; import 'package:git_touch/widgets/table_view.dart'; import 'package:git_touch/widgets/user_item.dart'; -import 'package:primer/primer.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:share/share.dart'; import 'package:github_contributions/github_contributions.dart'; @@ -11,7 +10,6 @@ import 'package:git_touch/models/settings.dart'; import 'package:provider/provider.dart'; import '../scaffolds/refresh.dart'; import '../widgets/entry_item.dart'; -import '../widgets/list_group.dart'; import '../widgets/repo_item.dart'; import '../widgets/link.dart'; import '../widgets/action.dart'; @@ -156,26 +154,22 @@ class UserScreen extends StatelessWidget { fullscreenDialog: true, ); } else { - List actions = []; - - if (payload['viewerCanFollow']) { - actions.add(MyAction( - text: payload['viewerIsFollowing'] ? 'Unfollow' : 'Follow', - onPress: () async { - if (payload['viewerIsFollowing']) { - await Provider.of(context) - .deleteWithCredentials('/user/following/$login'); - payload['viewerIsFollowing'] = false; - } else { - Provider.of(context) - .putWithCredentials('/user/following/$login'); - payload['viewerIsFollowing'] = true; - } - }, - )); - } - - actions.addAll([ + return ActionButton(title: 'User Actions', actions: [ + if (payload['viewerCanFollow']) + MyAction( + text: payload['viewerIsFollowing'] ? 'Unfollow' : 'Follow', + onPress: () async { + if (payload['viewerIsFollowing']) { + await Provider.of(context) + .deleteWithCredentials('/user/following/$login'); + payload['viewerIsFollowing'] = false; + } else { + Provider.of(context) + .putWithCredentials('/user/following/$login'); + payload['viewerIsFollowing'] = true; + } + }, + ), MyAction( text: 'Share', onPress: () { @@ -189,8 +183,6 @@ class UserScreen extends StatelessWidget { }, ), ]); - - return ActionButton(title: 'User Actions', actions: actions); } }, bodyBuilder: (data) { @@ -211,13 +203,12 @@ class UserScreen extends StatelessWidget { EntryItem( count: payload['repositories']['totalCount'], text: 'Repositories', - screenBuilder: (context) => ReposScreen(login: login), + screenBuilder: (context) => ReposScreen(login), ), EntryItem( count: payload['starredRepositories']['totalCount'], text: 'Stars', - screenBuilder: (context) => - ReposScreen(login: login, star: true), + screenBuilder: (context) => ReposScreen(login, star: true), ), EntryItem( count: payload['followers']['totalCount'],