feat: github oauth login
This commit is contained in:
parent
5c4d29c522
commit
543f8c82ea
|
@ -69,5 +69,3 @@ build/
|
|||
!**/ios/**/default.pbxuser
|
||||
!**/ios/**/default.perspectivev3
|
||||
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
||||
|
||||
lib/token.dart
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NotificationProvider extends StatefulWidget {
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
export 'notification.dart';
|
||||
export 'search.dart';
|
||||
export 'user.dart';
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {}
|
||||
|
|
|
@ -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]);
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
///
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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)
|
||||
];
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
export 'avatar.dart';
|
||||
export 'user_name.dart';
|
||||
export 'timeline_item.dart';
|
||||
export 'comment_item.dart';
|
Loading…
Reference in New Issue