feat: github oauth login

This commit is contained in:
Rongjian Zhang 2019-02-07 14:35:19 +08:00
parent 5c4d29c522
commit 543f8c82ea
31 changed files with 749 additions and 625 deletions

2
.gitignore vendored
View File

@ -69,5 +69,3 @@ build/
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
lib/token.dart

View File

@ -1,14 +1,16 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'providers/providers.dart';
import 'providers/notification.dart';
import 'providers/settings.dart';
import 'screens/screens.dart';
import 'screens/inbox.dart';
import 'screens/news.dart';
import 'screens/notifications.dart';
import 'screens/search.dart';
import 'screens/profile.dart';
import 'screens/login.dart';
class Home extends StatefulWidget {
@override
createState() => _HomeState();
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
@ -70,8 +72,18 @@ class _HomeState extends State<Home> {
@override
Widget build(BuildContext context) {
switch (SettingsProvider.of(context).layout) {
case LayoutMap.cupertino:
var settings = SettingsProvider.of(context);
if (!settings.ready) {
return MaterialApp(home: Scaffold(body: Text('a')));
}
if (settings.activeLogin == null) {
return LoginScreen();
}
switch (settings.theme) {
case ThemeMap.cupertino:
return CupertinoApp(
home: CupertinoTheme(
data: CupertinoThemeData(
@ -111,27 +123,18 @@ class _HomeState extends State<Home> {
}
class App extends StatelessWidget {
final isIos = Platform.isIOS;
final SearchBloc searchBloc;
App(this.searchBloc);
@override
build(context) {
return SearchProvider(
bloc: searchBloc,
child: NotificationProvider(
child: SettingsProvider(
child: DefaultTextStyle(
// style: TextStyle(color: Color(0xff24292e)),
style: TextStyle(color: Colors.black),
child: Home(),
// theme: ThemeData(
// textTheme: TextTheme(
// title: TextStyle(color: Colors.red),
// ),
// ),
),
return NotificationProvider(
child: SettingsProvider(
child: DefaultTextStyle(
style: TextStyle(color: Colors.black),
child: Home(),
// theme: ThemeData(
// textTheme: TextTheme(
// title: TextStyle(color: Colors.red),
// ),
// ),
),
),
);
@ -139,7 +142,14 @@ class App extends StatelessWidget {
}
void main() async {
SearchBloc searchBloc = SearchBloc();
// Platform messages may fail, so we use a try/catch PlatformException.
// try {
// String initialLink = await getInitialLink();
// print(initialLink);
// } on PlatformException {
// print('test');
// }
// DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
// AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
@ -148,5 +158,5 @@ void main() async {
// IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
// print('Running on ${iosInfo.utsname.machine}'); // e.g. "iPod7,1"
runApp(App(searchBloc));
runApp(App());
}

View File

@ -1,4 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart';
class NotificationProvider extends StatefulWidget {

View File

@ -1,3 +0,0 @@
export 'notification.dart';
export 'search.dart';
export 'user.dart';

View File

@ -4,34 +4,34 @@ import 'package:rxdart/subjects.dart';
import 'package:rxdart/rxdart.dart';
import '../utils/utils.dart';
Future search(String keyword, String type) async {
var data = await query('''
{
search(query: "$keyword", type: $type, first: 10) {
nodes {
... on User {
avatarUrl
login
}
... on Repository {
nameWithOwner
url
description
forkCount
stargazers {
totalCount
}
primaryLanguage {
name
color
}
}
}
}
}
''');
return data['search']['nodes'];
}
// Future search(String keyword, String type) async {
// var data = await query('''
// {
// search(query: "$keyword", type: $type, first: 10) {
// nodes {
// ... on User {
// avatarUrl
// login
// }
// ... on Repository {
// nameWithOwner
// url
// description
// forkCount
// stargazers {
// totalCount
// }
// primaryLanguage {
// name
// color
// }
// }
// }
// }
// }
// ''');
// return data['search']['nodes'];
// }
class SearchBloc {
final _keyword = BehaviorSubject(seedValue: '');
@ -62,7 +62,7 @@ class SearchBloc {
_querySearch(_) async {
_loading.add(true);
await search(_keyword.value, _getTypeByIndex(_active.value));
// await search(_keyword.value, _getTypeByIndex(_active.value));
_loading.add(false);
}

View File

@ -1,9 +1,39 @@
import 'dart:io';
import 'dart:convert';
import 'dart:async';
import 'package:http/http.dart' as http;
import 'package:uni_links/uni_links.dart';
import 'package:nanoid/nanoid.dart';
// import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../utils/utils.dart';
class LayoutMap {
class ThemeMap {
static const material = 0;
static const cupertino = 1;
static const all = [0, 1];
}
final prefix = 'https://api.github.com';
class Account {
String avatarUrl;
String token;
Account({this.avatarUrl, this.token});
Account.fromJson(input) {
avatarUrl = input['avatarUrl'];
token = input['token'];
}
Map<String, dynamic> toJson() {
return {
'avatarUrl': avatarUrl,
'token': token,
};
}
}
class SettingsProvider extends StatefulWidget {
@ -22,15 +52,176 @@ class SettingsProvider extends StatefulWidget {
}
class _SettingsProviderState extends State<SettingsProvider> {
int layout;
bool ready = false;
int theme;
Map<String, Account> githubAccountMap;
String activeLogin;
StreamSubscription<Uri> _sub;
get token {
if (activeLogin == null) {
return null;
}
return githubAccountMap[activeLogin].token;
}
@override
void initState() {
super.initState();
if (Platform.isIOS) {
layout = LayoutMap.cupertino;
_initDataFromPref();
_sub = getUriLinksStream().listen(_loginWithGithub, onError: (err) {
print(err);
});
}
@override
void dispose() {
super.dispose();
_sub.cancel();
}
// https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#web-application-flow
void _loginWithGithub(Uri uri) async {
// get token by code
var code = uri.queryParameters['code'];
// print(code);
var res = await http.post(
'https://github.com/login/oauth/access_token',
headers: {
HttpHeaders.acceptHeader: 'application/json',
HttpHeaders.contentTypeHeader: 'application/json',
},
body: json.encode({
'client_id': clientId,
'client_secret': clientSecret,
'code': code,
'state': randomString,
}),
);
print(res.body);
var data = json.decode(res.body);
String _token = data['access_token'];
// get login and avatar url
var queryData = await query('''
{
viewer {
login
avatarUrl
}
}
''', _token);
String login = queryData['viewer']['login'];
String avatarUrl = queryData['viewer']['avatarUrl'];
githubAccountMap[login] = Account(avatarUrl: avatarUrl, token: _token);
// write
SharedPreferences prefs = await SharedPreferences.getInstance();
var githubData = json.encode(githubAccountMap
.map((login, account) => MapEntry(login, account.toJson())));
print('write github: $githubData');
await prefs.setString('github', githubData);
setState(() {});
}
void _initDataFromPref() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
// read GitHub accounts
try {
var str = prefs.getString('github');
print('read github: $str');
Map<String, dynamic> github = json.decode(str);
githubAccountMap = github.map<String, Account>((login, _accountMap) =>
MapEntry(login, Account.fromJson(_accountMap)));
} catch (err) {
print(err);
githubAccountMap = {};
}
int _theme = prefs.getInt('theme');
if (ThemeMap.all.contains(_theme)) {
theme = _theme;
} else if (Platform.isIOS) {
theme = ThemeMap.cupertino;
}
// layout = LayoutMap.material;
setState(() {
ready = true;
});
// print(counter);
// await prefs.setInt('counter', counter);
}
void setActiveAccount(String login) {
setState(() {
activeLogin = login;
});
}
Future<dynamic> query(String query, [String _token]) async {
if (_token == null) {
_token = token;
}
if (_token == null) {
throw Exception('token is null');
}
final res = await http.post(prefix + '/graphql',
headers: {
HttpHeaders.authorizationHeader: 'token $_token',
HttpHeaders.contentTypeHeader: 'application/json'
},
body: json.encode({'query': query}));
final data = json.decode(res.body);
if (data['errors'] != null) {
throw new Exception(data['errors'].toString());
}
// print(data);
return data['data'];
}
Future<dynamic> getWithCredentials(String url, {String contentType}) async {
var headers = {HttpHeaders.authorizationHeader: 'token $token'};
if (contentType != null) {
// https://developer.github.com/v3/repos/contents/#custom-media-types
headers[HttpHeaders.contentTypeHeader] = contentType;
}
final res = await http.get(
prefix + url,
headers: headers,
);
print(res.body);
final data = json.decode(res.body);
return data;
}
Future<dynamic> patchWithCredentials(String url) async {
var headers = {HttpHeaders.authorizationHeader: 'token $token'};
await http.patch(prefix + url, headers: headers);
return true;
}
Future<dynamic> putWithCredentials(String url,
{String contentType, String body}) async {
var headers = {HttpHeaders.authorizationHeader: 'token $token'};
final res =
await http.put(prefix + url, headers: headers, body: body ?? {});
final data = json.decode(res.body);
return data;
}
String randomString;
generateRandomString() {
randomString = nanoid();
return randomString;
}
@override

View File

@ -4,32 +4,32 @@ import 'package:rxdart/subjects.dart';
import 'package:rxdart/rxdart.dart';
import '../utils/utils.dart';
Future queryUser(String login) async {
var data = await query('''
{
user(login: "$login") {
name
avatarUrl
bio
email
repositories {
totalCount
}
starredRepositories {
totalCount
}
followers {
totalCount
}
following {
totalCount
}
}
}
// Future queryUser(String login) async {
// var data = await query('''
// {
// user(login: "$login") {
// name
// avatarUrl
// bio
// email
// repositories {
// totalCount
// }
// starredRepositories {
// totalCount
// }
// followers {
// totalCount
// }
// following {
// totalCount
// }
// }
// }
''');
return data['user'];
}
// ''');
// return data['user'];
// }
class UserBloc {
Map<String, dynamic> _userDict = {};
@ -37,9 +37,9 @@ class UserBloc {
final _user = BehaviorSubject(seedValue: null);
fetchUser(String login) async {
var user = await queryUser(login);
_userDict[login] = user;
return user;
// var user = await queryUser(login);
// _userDict[login] = user;
// return user;
}
UserBloc() {}

View File

@ -1,193 +1,193 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import '../widgets/list_scaffold.dart';
import '../utils/utils.dart';
import '../screens/issue.dart';
import '../screens/pull_request.dart';
import '../widgets/link.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter/cupertino.dart';
// import '../widgets/list_scaffold.dart';
// import '../utils/utils.dart';
// import '../screens/issue.dart';
// import '../screens/pull_request.dart';
// import '../widgets/link.dart';
class NotificationPayload {
String type;
String owner;
String name;
int number;
String title;
String updateAt;
bool unread;
// class NotificationPayload {
// String type;
// String owner;
// String name;
// int number;
// String title;
// String updateAt;
// bool unread;
NotificationPayload.fromJson(input) {
type = input['subject']['type'];
name = input['repository']['name'];
owner = input['repository']['owner']['login'];
// NotificationPayload.fromJson(input) {
// type = input['subject']['type'];
// name = input['repository']['name'];
// owner = input['repository']['owner']['login'];
String url = input['subject']['url'];
String numberStr = url.split('/').lastWhere((_) => true);
number = int.parse(numberStr);
// String url = input['subject']['url'];
// String numberStr = url.split('/').lastWhere((_) => true);
// number = int.parse(numberStr);
title = input['subject']['title'];
updateAt = TimeAgo.formatFromString(input['updated_at']);
unread = input['unread'];
}
}
// title = input['subject']['title'];
// updateAt = TimeAgo.formatFromString(input['updated_at']);
// unread = input['unread'];
// }
// }
class NotificationItem extends StatelessWidget {
const NotificationItem({
Key key,
@required this.payload,
}) : super(key: key);
// class NotificationItem extends StatelessWidget {
// const NotificationItem({
// Key key,
// @required this.payload,
// }) : super(key: key);
final NotificationPayload payload;
// final NotificationPayload payload;
Widget _buildRoute() {
switch (payload.type) {
case 'Issue':
return IssueScreen(payload.number, payload.owner, payload.name);
case 'PullRequest':
return PullRequestScreen(payload.number, payload.owner, payload.name);
default:
// throw new Exception('Unhandled notification type: $type');
return Text('test');
}
}
// Widget _buildRoute() {
// switch (payload.type) {
// case 'Issue':
// return IssueScreen(payload.number, payload.owner, payload.name);
// case 'PullRequest':
// return PullRequestScreen(payload.number, payload.owner, payload.name);
// default:
// // throw new Exception('Unhandled notification type: $type');
// return Text('test');
// }
// }
IconData _buildIconData() {
switch (payload.type) {
case 'Issue':
return Octicons.issue_opened;
// color: Color.fromRGBO(0x28, 0xa7, 0x45, 1),
case 'PullRequest':
return Octicons.git_pull_request;
// color: Color.fromRGBO(0x6f, 0x42, 0xc1, 1),
default:
return Octicons.person;
}
}
// IconData _buildIconData() {
// switch (payload.type) {
// case 'Issue':
// return Octicons.issue_opened;
// // color: Color.fromRGBO(0x28, 0xa7, 0x45, 1),
// case 'PullRequest':
// return Octicons.git_pull_request;
// // color: Color.fromRGBO(0x6f, 0x42, 0xc1, 1),
// default:
// return Octicons.person;
// }
// }
@override
Widget build(BuildContext context) {
return Link(
onTap: () {
Navigator.of(context).push(
CupertinoPageRoute(builder: (context) => _buildRoute()),
);
},
child: Container(
padding: EdgeInsets.all(8),
// color: payload.unread ? Colors.white : Colors.black12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
padding: EdgeInsets.only(right: 8, top: 20),
child: Icon(_buildIconData(), color: Colors.black45),
),
Expanded(
child: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
payload.owner +
'/' +
payload.name +
' #' +
payload.number.toString(),
style: TextStyle(fontSize: 13, color: Colors.black54),
),
Padding(padding: EdgeInsets.only(top: 4)),
Text(
payload.title,
style: TextStyle(fontSize: 15),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
Padding(padding: EdgeInsets.only(top: 6)),
Text(
payload.updateAt,
style: TextStyle(
fontSize: 12,
// fontWeight: FontWeight.w300,
color: Colors.black54,
),
)
],
),
),
),
Column(
children: <Widget>[
Icon(Octicons.check, color: Colors.black45),
Icon(Octicons.unmute, color: Colors.black45)
],
),
],
),
],
),
),
);
}
}
// @override
// Widget build(BuildContext context) {
// return Link(
// onTap: () {
// Navigator.of(context).push(
// CupertinoPageRoute(builder: (context) => _buildRoute()),
// );
// },
// child: Container(
// padding: EdgeInsets.all(8),
// // color: payload.unread ? Colors.white : Colors.black12,
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: <Widget>[
// Row(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: <Widget>[
// Container(
// padding: EdgeInsets.only(right: 8, top: 20),
// child: Icon(_buildIconData(), color: Colors.black45),
// ),
// Expanded(
// child: Container(
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: <Widget>[
// Text(
// payload.owner +
// '/' +
// payload.name +
// ' #' +
// payload.number.toString(),
// style: TextStyle(fontSize: 13, color: Colors.black54),
// ),
// Padding(padding: EdgeInsets.only(top: 4)),
// Text(
// payload.title,
// style: TextStyle(fontSize: 15),
// maxLines: 3,
// overflow: TextOverflow.ellipsis,
// ),
// Padding(padding: EdgeInsets.only(top: 6)),
// Text(
// payload.updateAt,
// style: TextStyle(
// fontSize: 12,
// // fontWeight: FontWeight.w300,
// color: Colors.black54,
// ),
// )
// ],
// ),
// ),
// ),
// Column(
// children: <Widget>[
// Icon(Octicons.check, color: Colors.black45),
// Icon(Octicons.unmute, color: Colors.black45)
// ],
// ),
// ],
// ),
// ],
// ),
// ),
// );
// }
// }
Future<List<NotificationPayload>> fetchNotifications(int page) async {
List items =
await getWithCredentials('/notifications?page=$page&per_page=20');
return items.map((item) => NotificationPayload.fromJson(item)).toList();
}
// Future<List<NotificationPayload>> fetchNotifications(int page) async {
// List items =
// await getWithCredentials('/notifications?page=$page&per_page=20');
// return items.map((item) => NotificationPayload.fromJson(item)).toList();
// }
/// [@deprecated]
class InboxScreen extends StatefulWidget {
@override
_InboxScreenState createState() => _InboxScreenState();
}
// /// [@deprecated]
// class InboxScreen extends StatefulWidget {
// @override
// _InboxScreenState createState() => _InboxScreenState();
// }
class _InboxScreenState extends State<InboxScreen> {
// int active = 0;
int page = 0;
var payload;
List<NotificationPayload> _items = [];
// class _InboxScreenState extends State<InboxScreen> {
// // int active = 0;
// int page = 0;
// var payload;
// List<NotificationPayload> _items = [];
final titleMap = {
0: 'Unread',
1: 'Participating',
2: 'All',
};
// final titleMap = {
// 0: 'Unread',
// 1: 'Participating',
// 2: 'All',
// };
Future<void> _refresh() async {
page = 1;
var items = await fetchNotifications(page);
setState(() {
_items = items;
});
}
// Future<void> _refresh() async {
// page = 1;
// var items = await fetchNotifications(page);
// setState(() {
// _items = items;
// });
// }
@override
Widget build(BuildContext context) {
return ListScaffold(
title: Text('Inbox'),
trailingIconData: Octicons.check,
trailingOnTap: () async {
bool answer = await showConfim(context, 'Mark all as read?');
if (answer == true) {
await putWithCredentials('/notifications');
_refresh();
}
},
onRefresh: _refresh,
onLoadMore: () async {
page = page + 1;
var items = await fetchNotifications(page);
setState(() {
_items.addAll(items);
});
},
itemCount: _items.length,
itemBuilder: (context, index) {
return NotificationItem(payload: _items[index]);
},
);
}
}
// @override
// Widget build(BuildContext context) {
// return ListScaffold(
// title: Text('Inbox'),
// trailingIconData: Octicons.check,
// trailingOnTap: () async {
// bool answer = await showConfim(context, 'Mark all as read?');
// if (answer == true) {
// await putWithCredentials('/notifications');
// _refresh();
// }
// },
// onRefresh: _refresh,
// onLoadMore: () async {
// page = page + 1;
// var items = await fetchNotifications(page);
// setState(() {
// _items.addAll(items);
// });
// },
// itemCount: _items.length,
// itemBuilder: (context, index) {
// return NotificationItem(payload: _items[index]);
// },
// );
// }
// }

View File

@ -4,28 +4,7 @@ import '../utils/utils.dart';
import '../widgets/list_scaffold.dart';
import '../widgets/timeline_item.dart';
import '../widgets/comment_item.dart';
Future queryIssue(int id, String owner, String name) async {
var data = await query('''
{
repository(owner: "$owner", name: "$name") {
issue(number: $id) {
$graphqlChunk1
timeline(first: $pageSize) {
pageInfo {
hasNextPage
endCursor
}
nodes {
$graghqlChunk
}
}
}
}
}
''');
return data['repository']['issue'];
}
import '../providers/settings.dart';
class IssueScreen extends StatefulWidget {
final int id;
@ -67,6 +46,29 @@ class _IssueScreenState extends State<IssueScreen> {
List get _items => payload == null ? [] : payload['timeline']['nodes'];
Future queryIssue(
BuildContext context, int id, String owner, String name) async {
var data = await SettingsProvider.of(context).query('''
{
repository(owner: "$owner", name: "$name") {
issue(number: $id) {
$graphqlChunk1
timeline(first: $pageSize) {
pageInfo {
hasNextPage
endCursor
}
nodes {
$graghqlChunk
}
}
}
}
}
''');
return data['repository']['issue'];
}
@override
Widget build(BuildContext context) {
return ListScaffold(
@ -75,7 +77,8 @@ class _IssueScreenState extends State<IssueScreen> {
itemCount: _items.length,
itemBuilder: (context, index) => TimelineItem(_items[index], payload),
onRefresh: () async {
var _payload = await queryIssue(widget.id, widget.owner, widget.name);
var _payload =
await queryIssue(context, widget.id, widget.owner, widget.name);
if (mounted) {
setState(() {
payload = _payload;

43
lib/screens/login.dart Normal file
View File

@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import '../providers/settings.dart';
import '../utils/utils.dart';
class LoginScreen extends StatefulWidget {
@override
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
@override
Widget build(BuildContext context) {
var settings = SettingsProvider.of(context);
List<Widget> children =
settings.githubAccountMap.entries.map<Widget>((entry) {
return RaisedButton(
child: Text(entry.key),
onPressed: () {
settings.setActiveAccount(entry.key);
},
);
}).toList();
children.add(RaisedButton(
child: Text('Login'),
onPressed: () {
var state = settings.generateRandomString();
launch(
'https://github.com/login/oauth/authorize?client_id=$clientId&redirect_uri=gittouch://login&scope=user%20repo&state=$state',
forceSafariVC: false, // this makes URL Scheme work
);
},
));
return MaterialApp(
home: Scaffold(
body: Center(child: Column(children: children)),
),
);
}
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import '../widgets/list_scaffold.dart';
import '../widgets/event_item.dart';
import '../providers/settings.dart';
import '../utils/utils.dart';
class NewsScreen extends StatefulWidget {
@ -13,6 +14,13 @@ class NewsScreenState extends State<NewsScreen> {
int page = 1;
List<Event> _events = [];
Future<List<Event>> fetchEvents(BuildContext context, int page) async {
List data = await SettingsProvider.of(context).getWithCredentials(
'/users/pd4d10/received_events/public?page=$page',
);
return data.map<Event>((item) => Event.fromJSON(item)).toList();
}
@override
Widget build(context) {
return ListScaffold(
@ -21,14 +29,14 @@ class NewsScreenState extends State<NewsScreen> {
itemBuilder: (context, index) => EventItem(_events[index]),
onRefresh: () async {
page = 1;
var items = await fetchEvents(page);
var items = await fetchEvents(context, page);
setState(() {
_events = items;
});
},
onLoadMore: () async {
page = page + 1;
var items = await fetchEvents(page);
var items = await fetchEvents(context, page);
setState(() {
_events.addAll(items);
});

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import '../widgets/refresh_scaffold.dart';
import '../providers/notification.dart';
import '../providers/settings.dart';
import '../widgets/notification_item.dart';
import '../widgets/list_group.dart';
import '../widgets/link.dart';
@ -15,73 +16,6 @@ String getItemKey(NotificationPayload item) {
return '_' + item.number.toString();
}
Future<Map<String, NotificationGroup>> fetchNotifications(
int index, BuildContext context) async {
List items = await getWithCredentials(
'/notifications?all=${index == 2}&participating=${index == 1}');
var ns = items.map((item) => NotificationPayload.fromJson(item)).toList();
if (index == 0) {
NotificationProvider.of(context).setCount(ns.length);
}
Map<String, NotificationGroup> _groupMap = {};
ns.forEach((item) {
String repo = item.owner + '/' + item.name;
if (_groupMap[repo] == null) {
_groupMap[repo] = NotificationGroup(item.owner, item.name);
}
_groupMap[repo].items.add(item);
});
var schema = '{';
_groupMap.forEach((repo, group) {
var repoKey = getRepoKey(group);
schema +=
'$repoKey: repository(owner: "${group.owner}", name: "${group.name}") {';
group.items.forEach((item) {
var key = getItemKey(item);
switch (item.type) {
case 'Issue':
schema += '''
$key: issue(number: ${item.number}) {
state
}
''';
break;
case 'PullRequest':
schema += '''
$key: pullRequest(number: ${item.number}) {
state
}
''';
break;
}
});
schema += '}';
});
schema += '}';
// print(schema);
var data = await query(schema);
_groupMap.forEach((repo, group) {
group.items.forEach((item) {
var itemData = data[getRepoKey(group)][getItemKey(item)];
if (itemData != null) {
item.state = itemData['state'];
}
});
});
// print(data);
return _groupMap;
}
class NotificationGroup {
String owner;
String name;
@ -107,7 +41,74 @@ class NotificationScreenState extends State<NotificationScreen> {
_refresh();
}
Widget _buildGroupItem(MapEntry<String, NotificationGroup> entry) {
Future<Map<String, NotificationGroup>> fetchNotifications(int index) async {
List items = await SettingsProvider.of(context).getWithCredentials(
'/notifications?all=${index == 2}&participating=${index == 1}');
var ns = items.map((item) => NotificationPayload.fromJson(item)).toList();
if (index == 0) {
NotificationProvider.of(context).setCount(ns.length);
}
Map<String, NotificationGroup> _groupMap = {};
ns.forEach((item) {
String repo = item.owner + '/' + item.name;
if (_groupMap[repo] == null) {
_groupMap[repo] = NotificationGroup(item.owner, item.name);
}
_groupMap[repo].items.add(item);
});
var schema = '{';
_groupMap.forEach((repo, group) {
var repoKey = getRepoKey(group);
schema +=
'$repoKey: repository(owner: "${group.owner}", name: "${group.name}") {';
group.items.forEach((item) {
var key = getItemKey(item);
switch (item.type) {
case 'Issue':
schema += '''
$key: issue(number: ${item.number}) {
state
}
''';
break;
case 'PullRequest':
schema += '''
$key: pullRequest(number: ${item.number}) {
state
}
''';
break;
}
});
schema += '}';
});
schema += '}';
// print(schema);
var data = await SettingsProvider.of(context).query(schema);
_groupMap.forEach((repo, group) {
group.items.forEach((item) {
var itemData = data[getRepoKey(group)][getItemKey(item)];
if (itemData != null) {
item.state = itemData['state'];
}
});
});
// print(data);
return _groupMap;
}
Widget _buildGroupItem(
BuildContext context, MapEntry<String, NotificationGroup> entry) {
var group = entry.value;
var repo = group.repo;
return ListGroup(
@ -120,8 +121,9 @@ class NotificationScreenState extends State<NotificationScreen> {
),
Link(
onTap: () async {
await putWithCredentials('/repos/$repo/notifications');
await _refresh();
// await SettingsProvider.of(context)
// .putWithCredentials('/repos/$repo/notifications');
// await _refresh();
},
child: Icon(
Octicons.check,
@ -144,13 +146,13 @@ class NotificationScreenState extends State<NotificationScreen> {
});
}
Future<void> _onSwitchTab(BuildContext context, int index) async {
Future<void> _onSwitchTab(int index) async {
setState(() {
active = index;
loading = true;
});
var _groupMap = await fetchNotifications(active, context);
var _groupMap = await fetchNotifications(active);
if (mounted) {
setState(() {
@ -161,7 +163,8 @@ class NotificationScreenState extends State<NotificationScreen> {
}
Future<void> _refresh() async {
await _onSwitchTab(context, active);
// setState(() {});
await _onSwitchTab(active);
}
var textMap = {
@ -199,13 +202,13 @@ class NotificationScreenState extends State<NotificationScreen> {
);
},
);
_onSwitchTab(context, value);
_onSwitchTab(value);
},
),
actions: <Widget>[
PopupMenuButton(
onSelected: (value) {
_onSwitchTab(context, value);
_onSwitchTab(value);
},
itemBuilder: (context) {
return textMap.entries.map((entry) {
@ -217,7 +220,10 @@ class NotificationScreenState extends State<NotificationScreen> {
onRefresh: _refresh,
loading: loading,
bodyBuilder: () {
return Column(children: groupMap.entries.map(_buildGroupItem).toList());
return Column(
children: groupMap.entries
.map((entry) => _buildGroupItem(context, entry))
.toList());
},
);
}

View File

@ -1,12 +1,31 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import '../providers/settings.dart';
import '../utils/utils.dart';
import '../widgets/list_scaffold.dart';
import '../widgets/timeline_item.dart';
import '../widgets/comment_item.dart';
Future queryPullRequest(int id, String owner, String name) async {
var data = await query('''
class PullRequestScreen extends StatefulWidget {
final int id;
final String owner;
final String name;
PullRequestScreen(this.id, this.owner, this.name);
@override
_PullRequestScreenState createState() => _PullRequestScreenState();
}
class _PullRequestScreenState extends State<PullRequestScreen> {
Map<String, dynamic> payload;
Future queryPullRequest(BuildContext context) async {
var owner = widget.owner;
var id = widget.id;
var name = widget.name;
var data = await SettingsProvider.of(context).query('''
{
repository(owner: "$owner", name: "$name") {
pullRequest(number: $id) {
@ -67,22 +86,8 @@ Future queryPullRequest(int id, String owner, String name) async {
}
}
''');
return data['repository']['pullRequest'];
}
class PullRequestScreen extends StatefulWidget {
final int id;
final String owner;
final String name;
PullRequestScreen(this.id, this.owner, this.name);
@override
_PullRequestScreenState createState() => _PullRequestScreenState();
}
class _PullRequestScreenState extends State<PullRequestScreen> {
Map<String, dynamic> payload;
return data['repository']['pullRequest'];
}
Widget _buildBadge() {
bool merged = payload['merged'];
@ -145,8 +150,7 @@ class _PullRequestScreenState extends State<PullRequestScreen> {
itemCount: _items.length,
itemBuilder: (context, index) => TimelineItem(_items[index], payload),
onRefresh: () async {
var _payload =
await queryPullRequest(widget.id, widget.owner, widget.name);
var _payload = await queryPullRequest(context);
if (mounted) {
setState(() {
payload = _payload;

View File

@ -2,22 +2,38 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import '../providers/settings.dart';
import '../widgets/refresh_scaffold.dart';
import '../utils/utils.dart';
import '../widgets/repo_item.dart';
import '../widgets/entry_item.dart';
import '../screens/issues.dart';
import '../screens/pull_requests.dart';
Future fetchReadme(String owner, String name) async {
var data = await getWithCredentials('/repos/$owner/$name/readme');
var bits = base64.decode(data['content'].replaceAll('\n', ''));
var str = utf8.decode(bits);
return str;
class RepoScreen extends StatefulWidget {
final String owner;
final String name;
RepoScreen(this.owner, this.name);
@override
_RepoScreenState createState() => _RepoScreenState();
}
Future queryRepo(String owner, String name) async {
var data = await query('''
class _RepoScreenState extends State<RepoScreen> {
Map<String, dynamic> payload;
String readme;
bool loading;
@override
void initState() {
super.initState();
_refresh();
}
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") {
owner {
@ -47,28 +63,17 @@ Future queryRepo(String owner, String name) async {
}
''');
return data['repository'];
}
return data['repository'];
}
class RepoScreen extends StatefulWidget {
final String owner;
final String name;
RepoScreen(this.owner, this.name);
@override
_RepoScreenState createState() => _RepoScreenState();
}
class _RepoScreenState extends State<RepoScreen> {
Map<String, dynamic> payload;
String readme;
bool loading;
@override
void initState() {
super.initState();
_refresh();
Future fetchReadme(BuildContext context) async {
var owner = widget.owner;
var name = widget.name;
var data = await SettingsProvider.of(context)
.getWithCredentials('/repos/$owner/$name/readme');
var bits = base64.decode(data['content'].replaceAll('\n', ''));
var str = utf8.decode(bits);
return str;
}
Future<void> _refresh() async {
@ -76,8 +81,8 @@ class _RepoScreenState extends State<RepoScreen> {
loading = true;
});
List items = await Future.wait([
queryRepo(widget.owner, widget.name),
fetchReadme(widget.owner, widget.name),
queryRepo(context),
fetchReadme(context),
]);
setState(() {
loading = false;

View File

@ -1,8 +0,0 @@
export 'news.dart';
export 'user.dart';
export 'pull_request.dart';
export 'repo.dart';
export 'issue.dart';
export 'notifications.dart';
export 'search.dart';
export 'profile.dart';

View File

@ -13,8 +13,8 @@ class _SearchScreenState extends State<SearchScreen> {
@override
Widget build(BuildContext context) {
switch (SettingsProvider.of(context).layout) {
case LayoutMap.cupertino:
switch (SettingsProvider.of(context).theme) {
case ThemeMap.cupertino:
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: CupertinoTextField(

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import '../providers/settings.dart';
import '../widgets/refresh_scaffold.dart';
import '../widgets/avatar.dart';
import '../widgets/entry_item.dart';
@ -29,8 +30,30 @@ primaryLanguage {
}
''';
Future queryUser(String login) async {
var data = await query('''
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
GlobalKey<RefreshIndicatorState>();
class UserScreen extends StatefulWidget {
final String login;
UserScreen(this.login);
_UserScreenState createState() => _UserScreenState();
}
class _UserScreenState extends State<UserScreen> {
bool loading;
Map<String, dynamic> payload = {};
@override
void initState() {
super.initState();
_refresh();
}
Future queryUser(BuildContext context) async {
var login = widget.login;
var data = await SettingsProvider.of(context).query('''
{
user(login: "$login") {
name
@ -60,28 +83,7 @@ Future queryUser(String login) async {
}
}
''');
return data['user'];
}
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
GlobalKey<RefreshIndicatorState>();
class UserScreen extends StatefulWidget {
final String login;
UserScreen(this.login);
_UserScreenState createState() => _UserScreenState();
}
class _UserScreenState extends State<UserScreen> {
bool loading;
Map<String, dynamic> payload = {};
@override
void initState() {
super.initState();
_refresh();
return data['user'];
}
Widget _buildRepos() {
@ -106,7 +108,7 @@ class _UserScreenState extends State<UserScreen> {
setState(() {
loading = true;
});
var _payload = await queryUser(widget.login);
var _payload = await queryUser(context);
if (mounted) {
setState(() {
loading = false;

View File

@ -1,71 +1 @@
import 'dart:convert';
import 'dart:async';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:github/server.dart';
export 'package:github/server.dart';
import '../token.dart';
var ghClient = createGitHubClient(auth: Authentication.withToken(token));
final prefix = 'https://api.github.com';
final endpoint = '/graphql';
Future<dynamic> getWithCredentials(String url, {String contentType}) async {
var headers = {HttpHeaders.authorizationHeader: 'token $token'};
if (contentType != null) {
// https://developer.github.com/v3/repos/contents/#custom-media-types
headers[HttpHeaders.contentTypeHeader] = contentType;
}
final res = await http.get(
prefix + url,
headers: headers,
);
final data = json.decode(res.body);
return data;
}
Future<dynamic> postWithCredentials(String url, String body,
{String contentType}) async {
var headers = {HttpHeaders.authorizationHeader: 'token $token'};
if (contentType != null) {
headers[HttpHeaders.contentTypeHeader] = contentType;
}
final res = await http.post(prefix + url, headers: headers, body: body);
final data = json.decode(res.body);
return data;
}
Future<dynamic> putWithCredentials(String url,
{String contentType, String body}) async {
var headers = {HttpHeaders.authorizationHeader: 'token $token'};
if (contentType != null) {
headers[HttpHeaders.contentTypeHeader] = contentType;
}
final res = await http.put(prefix + url, headers: headers, body: body ?? {});
final data = json.decode(res.body);
return data;
}
Future<dynamic> patchWithCredentials(String url) async {
var headers = {HttpHeaders.authorizationHeader: 'token $token'};
await http.patch(prefix + url, headers: headers);
return true;
}
Future<dynamic> query(String query) async {
final res =
await postWithCredentials('/graphql', json.encode({'query': query}));
if (res['errors'] != null) {
throw new Exception(res['errors'].toString());
}
// print(res);
return res['data'];
}
Future<List<Event>> fetchEvents(int page) async {
List data = await getWithCredentials(
'/users/pd4d10/received_events/public?page=$page',
);
return data.map<Event>((item) => Event.fromJSON(item)).toList();
}

View File

@ -2,11 +2,15 @@ import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import '../providers/settings.dart';
import '../screens/screens.dart';
import '../screens/repo.dart';
export 'github.dart';
export 'octicons.dart';
export 'timeago.dart';
// These keys are for development
var clientId = '9b7d1cc04a1db5710767';
var clientSecret = '710e085908dde6a8b55f7a9dc447ad5c0c5617d1';
Color convertColor(String cssHex) {
if (cssHex.startsWith('#')) {
cssHex = cssHex.substring(1);
@ -21,8 +25,8 @@ class Option<T> {
}
Future<bool> showConfim(BuildContext context, String text) {
switch (SettingsProvider.of(context).layout) {
case LayoutMap.cupertino:
switch (SettingsProvider.of(context).theme) {
case ThemeMap.cupertino:
return showCupertinoDialog(
context: context,
builder: (context) {
@ -89,8 +93,8 @@ Future<T> showOptions<T>(BuildContext context, List<Option<T>> options) {
);
};
switch (SettingsProvider.of(context).layout) {
case LayoutMap.cupertino:
switch (SettingsProvider.of(context).theme) {
case ThemeMap.cupertino:
return showCupertinoDialog<T>(
context: context,
builder: builder,

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import '../screens/screens.dart';
import '../screens/user.dart';
import 'link.dart';
class Avatar extends StatelessWidget {

View File

@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import '../widgets/widgets.dart';
import '../utils/utils.dart';
import 'avatar.dart';
import 'user_name.dart';
class CommentItem extends StatelessWidget {
final Map<String, dynamic> item;

View File

@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import '../screens/screens.dart';
import '../screens/issue.dart';
import '../screens/pull_request.dart';
import '../screens/user.dart';
import '../utils/utils.dart';
import '../widgets/widgets.dart';
import 'avatar.dart';
/// Events types:
///

View File

@ -14,10 +14,9 @@ class Link extends StatelessWidget {
child: Ink(
color: bgColor ?? Colors.white,
child: InkWell(
splashColor:
SettingsProvider.of(context).layout == LayoutMap.cupertino
? Colors.transparent
: null,
splashColor: SettingsProvider.of(context).theme == ThemeMap.cupertino
? Colors.transparent
: null,
onTap: onTap,
child: child,
),

View File

@ -126,8 +126,8 @@ class _ListScaffoldState extends State<ListScaffold> {
@override
Widget build(BuildContext context) {
switch (SettingsProvider.of(context).layout) {
case LayoutMap.cupertino:
switch (SettingsProvider.of(context).theme) {
case ThemeMap.cupertino:
List<Widget> slivers = [
CupertinoSliverRefreshControl(onRefresh: widget.onRefresh)
];

View File

@ -8,8 +8,8 @@ class Loading extends StatelessWidget {
Loading({this.more});
Widget _buildIndicator(BuildContext context) {
switch (SettingsProvider.of(context).layout) {
case LayoutMap.cupertino:
switch (SettingsProvider.of(context).theme) {
case ThemeMap.cupertino:
return CupertinoActivityIndicator(radius: 12);
default:
return Center(

View File

@ -1,69 +0,0 @@
import 'package:flutter/cupertino.dart';
import '../utils/utils.dart';
typedef Future<void> Refresh();
typedef Widget BuildWithContent(
{List<Event> events, ScrollController controller, Refresh refresh});
class NewsProvider extends StatefulWidget {
final BuildWithContent build;
NewsProvider(this.build);
@override
NewsProviderState createState() => NewsProviderState();
}
class NewsProviderState extends State<NewsProvider> {
int page = 1;
bool loading = false;
List<Event> _events = [];
ScrollController _controller = ScrollController();
@override
void initState() {
super.initState();
_refresh();
_controller.addListener(() {
if (_controller.offset + 100 > _controller.position.maxScrollExtent &&
!_controller.position.outOfRange &&
!loading) {
_loadMore();
}
});
}
Future<void> _refresh() async {
setState(() {
loading = true;
});
page = 1;
var items = await fetchEvents(page);
setState(() {
loading = false;
_events = items;
});
}
_loadMore() async {
print('more');
setState(() {
loading = true;
});
page = page + 1;
var items = await fetchEvents(page);
setState(() {
loading = false;
_events.addAll(items);
});
}
@override
Widget build(context) {
return widget.build(
events: _events,
controller: _controller,
refresh: _refresh,
);
}
}

View File

@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart';
import '../utils/utils.dart';
import '../screens/issue.dart';
import '../screens/pull_request.dart';
import '../providers/settings.dart';
import 'link.dart';
class NotificationPayload {
@ -115,7 +116,8 @@ class _NotificationItemState extends State<NotificationItem> {
loading = true;
});
try {
await patchWithCredentials('/notifications/threads/' + payload.id);
await SettingsProvider.of(context)
.patchWithCredentials('/notifications/threads/' + payload.id);
widget.markAsRead();
} finally {
if (mounted) {

View File

@ -34,8 +34,8 @@ class RefreshScaffold extends StatelessWidget {
@override
Widget build(BuildContext context) {
switch (SettingsProvider.of(context).layout) {
case LayoutMap.cupertino:
switch (SettingsProvider.of(context).theme) {
case ThemeMap.cupertino:
return CupertinoPageScaffold(
navigationBar:
CupertinoNavigationBar(middle: title, trailing: trailing),

View File

@ -2,7 +2,8 @@ import 'dart:core';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import '../utils/utils.dart';
import '../widgets/widgets.dart';
import 'comment_item.dart';
import 'user_name.dart';
class TimelineItem extends StatelessWidget {
final Map<String, dynamic> item;

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import '../screens/screens.dart';
import '../screens/user.dart';
import 'link.dart';
final style = TextStyle(fontWeight: FontWeight.w600);

View File

@ -1,4 +0,0 @@
export 'avatar.dart';
export 'user_name.dart';
export 'timeline_item.dart';
export 'comment_item.dart';