feat: organization screen

This commit is contained in:
Rongjian Zhang 2019-09-22 01:02:14 +08:00
parent 3354ac4e6b
commit 45687a48a6
5 changed files with 117 additions and 204 deletions

View File

@ -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',
);
}
}

View File

@ -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<OrganizationScreen> {
Future query() async {
var login = widget.login;
var data = await Provider.of<SettingsModel>(context).query('''
OrganizationScreen(this.login);
Iterable<Widget> _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<SettingsModel>(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<OrganizationScreen> {
}
}
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: <Widget>[
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: <Widget>[
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: <Widget>[
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<OrganizationScreen> {
},
bodyBuilder: (payload) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Container(
padding: EdgeInsets.all(10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Avatar(url: payload['avatarUrl'], size: 28),
Padding(padding: EdgeInsets.only(left: 10)),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
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: <Widget>[
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),
],
);
},

View File

@ -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;

View File

@ -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<ReposScreen> {
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<ListPayload> _queryRepos([String cursor]) async {
Future<ListPayload> _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<SettingsModel>(context).query('''
{
$scope(login: "$login") {
@ -61,9 +56,9 @@ class _ReposScreenState extends State<ReposScreen> {
@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),
);
}

View File

@ -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<MyAction> actions = [];
if (payload['viewerCanFollow']) {
actions.add(MyAction(
text: payload['viewerIsFollowing'] ? 'Unfollow' : 'Follow',
onPress: () async {
if (payload['viewerIsFollowing']) {
await Provider.of<SettingsModel>(context)
.deleteWithCredentials('/user/following/$login');
payload['viewerIsFollowing'] = false;
} else {
Provider.of<SettingsModel>(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<SettingsModel>(context)
.deleteWithCredentials('/user/following/$login');
payload['viewerIsFollowing'] = false;
} else {
Provider.of<SettingsModel>(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'],