2019-01-31 07:20:48 +01:00
|
|
|
import 'dart:io';
|
2019-02-07 07:35:19 +01:00
|
|
|
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';
|
2019-02-11 17:22:58 +01:00
|
|
|
import 'package:url_launcher/url_launcher.dart';
|
2019-02-07 07:35:19 +01:00
|
|
|
// import 'package:flutter/services.dart';
|
2019-01-31 07:20:48 +01:00
|
|
|
import 'package:flutter/material.dart';
|
2019-03-10 14:53:35 +01:00
|
|
|
import 'package:flutter/cupertino.dart';
|
2019-02-07 07:35:19 +01:00
|
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
2019-02-21 14:21:16 +01:00
|
|
|
// import '../utils/utils.dart';
|
2019-02-09 11:20:14 +01:00
|
|
|
import '../utils/constants.dart';
|
2019-03-10 09:09:26 +01:00
|
|
|
import '../utils/utils.dart';
|
2019-02-21 11:43:41 +01:00
|
|
|
import '../models/account.dart';
|
|
|
|
|
2019-02-21 14:21:16 +01:00
|
|
|
class PlatformType {
|
2019-02-21 15:36:19 +01:00
|
|
|
static const github = 'github';
|
|
|
|
static const gitlab = 'gitlab';
|
|
|
|
}
|
|
|
|
|
2019-02-21 11:43:41 +01:00
|
|
|
// abstract class Model<T> {
|
|
|
|
// Future<T> query(BuildContext context) {
|
|
|
|
// var settings = SettingsProvider.of(context);
|
|
|
|
|
|
|
|
// switch (settings.platformType) {
|
|
|
|
// case PlatformType.github:
|
|
|
|
// return queryGithub(settings);
|
|
|
|
// case PlatformType.gitlab:
|
|
|
|
// return queryGitlab(settings);
|
|
|
|
// default:
|
|
|
|
// return null;
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
|
|
|
|
// Future<T> queryGithub(SettingsProviderState settings);
|
|
|
|
// Future<T> queryGitlab(SettingsProviderState settings);
|
|
|
|
// }
|
2019-01-30 07:46:18 +01:00
|
|
|
|
2019-02-07 07:35:19 +01:00
|
|
|
class ThemeMap {
|
2019-01-30 07:46:18 +01:00
|
|
|
static const material = 0;
|
|
|
|
static const cupertino = 1;
|
2019-02-21 14:21:16 +01:00
|
|
|
static const values = [0, 1];
|
2019-02-07 07:35:19 +01:00
|
|
|
}
|
|
|
|
|
2019-01-31 07:20:48 +01:00
|
|
|
class SettingsProvider extends StatefulWidget {
|
|
|
|
final Widget child;
|
|
|
|
|
|
|
|
SettingsProvider({@required this.child});
|
|
|
|
|
2019-02-21 11:43:41 +01:00
|
|
|
static SettingsProviderState of(BuildContext context) {
|
2019-01-31 07:20:48 +01:00
|
|
|
return (context.inheritFromWidgetOfExactType(_InheritedSettingsProvider)
|
|
|
|
as _InheritedSettingsProvider)
|
|
|
|
.data;
|
2019-01-30 07:46:18 +01:00
|
|
|
}
|
|
|
|
|
2019-01-31 07:20:48 +01:00
|
|
|
@override
|
2019-02-21 11:43:41 +01:00
|
|
|
SettingsProviderState createState() => new SettingsProviderState();
|
2019-01-30 07:46:18 +01:00
|
|
|
}
|
|
|
|
|
2019-02-21 11:43:41 +01:00
|
|
|
class SettingsProviderState extends State<SettingsProvider> {
|
2019-02-07 07:35:19 +01:00
|
|
|
bool ready = false;
|
|
|
|
int theme;
|
2019-02-21 11:43:41 +01:00
|
|
|
|
|
|
|
Map<String, AccountModel> githubAccountMap;
|
2019-02-21 15:36:19 +01:00
|
|
|
Map<String, Map<String, Map<String, AccountModel>>> accountMap;
|
2019-02-21 11:43:41 +01:00
|
|
|
|
2019-02-21 15:36:19 +01:00
|
|
|
String activePlatform;
|
|
|
|
String activeDomain;
|
2019-02-07 07:35:19 +01:00
|
|
|
String activeLogin;
|
2019-02-21 14:21:16 +01:00
|
|
|
|
2019-02-07 07:35:19 +01:00
|
|
|
StreamSubscription<Uri> _sub;
|
2019-02-10 06:17:25 +01:00
|
|
|
bool loading = false;
|
2019-02-07 07:35:19 +01:00
|
|
|
|
2019-02-21 11:43:41 +01:00
|
|
|
// PlatformType platformType;
|
|
|
|
|
|
|
|
String prefix = 'https://api.github.com';
|
|
|
|
|
2019-02-08 16:20:28 +01:00
|
|
|
Future<void> setTheme(int _theme) async {
|
|
|
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
|
|
|
|
|
|
theme = _theme;
|
2019-02-21 15:36:19 +01:00
|
|
|
await prefs.setInt(StorageKeys.theme, theme);
|
2019-02-09 06:29:44 +01:00
|
|
|
print('write theme: $theme');
|
2019-02-08 16:20:28 +01:00
|
|
|
|
|
|
|
setState(() {});
|
|
|
|
}
|
|
|
|
|
2019-02-21 14:21:16 +01:00
|
|
|
String get token {
|
|
|
|
if (activeLogin == null) return null;
|
|
|
|
|
|
|
|
switch (activePlatform) {
|
|
|
|
case PlatformType.github:
|
|
|
|
return githubAccountMap[activeLogin].token;
|
|
|
|
default:
|
2019-02-21 15:36:19 +01:00
|
|
|
return accountMap[activePlatform][activeDomain][activeLogin].token;
|
2019-02-07 07:35:19 +01:00
|
|
|
}
|
|
|
|
}
|
2019-01-30 07:46:18 +01:00
|
|
|
|
2019-01-31 07:20:48 +01:00
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
super.initState();
|
2019-02-07 07:35:19 +01:00
|
|
|
_initDataFromPref();
|
|
|
|
|
2019-02-08 12:44:10 +01:00
|
|
|
_sub = getUriLinksStream().listen(_onSchemeDetected, onError: (err) {
|
2019-02-07 07:35:19 +01:00
|
|
|
print(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void dispose() {
|
|
|
|
super.dispose();
|
|
|
|
_sub.cancel();
|
|
|
|
}
|
|
|
|
|
|
|
|
// https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#web-application-flow
|
2019-02-21 11:43:41 +01:00
|
|
|
Future<void> _onSchemeDetected(Uri uri) async {
|
2019-03-13 14:31:36 +01:00
|
|
|
await closeWebView();
|
2019-02-11 17:22:58 +01:00
|
|
|
|
2019-02-10 06:17:25 +01:00
|
|
|
setState(() {
|
|
|
|
loading = true;
|
|
|
|
});
|
|
|
|
|
2019-02-07 07:35:19 +01:00
|
|
|
// 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,
|
|
|
|
}),
|
|
|
|
);
|
2019-02-08 16:20:28 +01:00
|
|
|
// print(res.body);
|
2019-02-07 07:35:19 +01:00
|
|
|
var data = json.decode(res.body);
|
2019-02-21 11:43:41 +01:00
|
|
|
_loginWithToken(data['access_token']);
|
|
|
|
}
|
2019-02-07 07:35:19 +01:00
|
|
|
|
2019-02-21 11:43:41 +01:00
|
|
|
Future<void> _loginWithToken(String token) async {
|
2019-02-07 07:35:19 +01:00
|
|
|
// get login and avatar url
|
|
|
|
var queryData = await query('''
|
|
|
|
{
|
|
|
|
viewer {
|
|
|
|
login
|
|
|
|
avatarUrl
|
|
|
|
}
|
|
|
|
}
|
2019-02-21 11:43:41 +01:00
|
|
|
''', token);
|
2019-02-07 07:35:19 +01:00
|
|
|
String login = queryData['viewer']['login'];
|
|
|
|
String avatarUrl = queryData['viewer']['avatarUrl'];
|
|
|
|
|
2019-02-21 11:43:41 +01:00
|
|
|
githubAccountMap[login] = AccountModel(avatarUrl: avatarUrl, token: token);
|
2019-02-07 07:35:19 +01:00
|
|
|
|
|
|
|
// write
|
|
|
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
2019-02-21 14:21:16 +01:00
|
|
|
await prefs.setString(
|
2019-02-21 15:36:19 +01:00
|
|
|
StorageKeys.github,
|
2019-02-21 14:21:16 +01:00
|
|
|
json.encode(githubAccountMap
|
|
|
|
.map((login, account) => MapEntry(login, account.toJson()))));
|
2019-02-07 07:35:19 +01:00
|
|
|
|
2019-02-10 06:17:25 +01:00
|
|
|
setState(() {
|
|
|
|
loading = false;
|
|
|
|
});
|
2019-02-07 07:35:19 +01:00
|
|
|
}
|
|
|
|
|
2019-02-21 14:21:16 +01:00
|
|
|
Future<void> loginToGitlab(String domain, String token) async {
|
|
|
|
setState(() {
|
|
|
|
loading = true;
|
|
|
|
});
|
|
|
|
|
|
|
|
try {
|
|
|
|
var res = await http
|
|
|
|
.get('$domain/api/v4/user', headers: {'Private-Token': token});
|
|
|
|
var info = json.decode(res.body);
|
|
|
|
|
|
|
|
if (info['message'] != null) {
|
|
|
|
throw info['message'];
|
|
|
|
}
|
|
|
|
|
|
|
|
String login = info['username'];
|
|
|
|
String avatarUrl = info['avatar_url'];
|
2019-02-21 15:36:19 +01:00
|
|
|
|
|
|
|
if (accountMap[PlatformType.gitlab] == null)
|
|
|
|
accountMap[PlatformType.gitlab] = {};
|
|
|
|
if (accountMap[PlatformType.gitlab][domain] == null)
|
|
|
|
accountMap[PlatformType.gitlab][domain] = {};
|
|
|
|
|
|
|
|
accountMap[PlatformType.gitlab][domain][login] =
|
|
|
|
AccountModel(token: token, avatarUrl: avatarUrl);
|
2019-02-21 14:21:16 +01:00
|
|
|
|
|
|
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
2019-02-21 15:36:19 +01:00
|
|
|
|
|
|
|
String str = json.encode(accountMap.map((type, v0) {
|
|
|
|
return MapEntry(type, v0.map((domain, v1) {
|
|
|
|
return MapEntry(domain, v1.map((login, v2) {
|
|
|
|
return MapEntry(login, v2.toJson());
|
|
|
|
}));
|
|
|
|
}));
|
|
|
|
}));
|
|
|
|
await prefs.setString(StorageKeys.account, str);
|
2019-02-21 14:21:16 +01:00
|
|
|
} catch (err) {
|
|
|
|
print(err);
|
|
|
|
// TODO: show errors
|
|
|
|
} finally {
|
|
|
|
setState(() {
|
|
|
|
loading = false;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-07 07:35:19 +01:00
|
|
|
void _initDataFromPref() async {
|
|
|
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
|
|
|
|
|
|
// read GitHub accounts
|
|
|
|
try {
|
2019-02-21 15:36:19 +01:00
|
|
|
String str = prefs.getString(StorageKeys.github);
|
2019-02-21 14:21:16 +01:00
|
|
|
print('read github: $str');
|
|
|
|
|
|
|
|
Map<String, dynamic> data = json.decode(str ?? '{}');
|
|
|
|
githubAccountMap = data.map<String, AccountModel>((login, _accountMap) =>
|
|
|
|
MapEntry(login, AccountModel.fromJson(_accountMap)));
|
2019-02-07 07:35:19 +01:00
|
|
|
} catch (err) {
|
|
|
|
print(err);
|
|
|
|
githubAccountMap = {};
|
|
|
|
}
|
|
|
|
|
2019-02-21 15:36:19 +01:00
|
|
|
// read accounts
|
2019-02-21 14:21:16 +01:00
|
|
|
try {
|
2019-02-21 15:36:19 +01:00
|
|
|
String str = prefs.getString(StorageKeys.account);
|
|
|
|
print('read account: $str');
|
|
|
|
|
|
|
|
var data = Map<String, Map<String, dynamic>>.from(
|
|
|
|
Map<String, dynamic>.from(json.decode(str ?? '{}')));
|
|
|
|
|
|
|
|
accountMap = {};
|
|
|
|
data.forEach((platform, v0) {
|
|
|
|
accountMap[platform] = {};
|
|
|
|
v0.forEach((domain, v1) {
|
|
|
|
accountMap[platform][domain] = {};
|
|
|
|
v1.forEach((login, v2) {
|
|
|
|
accountMap[platform][domain][login] = AccountModel.fromJson(v2);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2019-02-21 14:21:16 +01:00
|
|
|
|
2019-02-21 15:36:19 +01:00
|
|
|
// TODO: type cast
|
|
|
|
// accountMap = data.map((type, v0) {
|
|
|
|
// return MapEntry(type, v0.map((domain, v1) {
|
|
|
|
// return MapEntry(domain, v1.map((login, v2) {
|
|
|
|
// return MapEntry(login, AccountModel.fromJson(v2));
|
|
|
|
// }));
|
|
|
|
// }));
|
|
|
|
// });
|
2019-02-21 14:21:16 +01:00
|
|
|
} catch (err) {
|
|
|
|
print(err);
|
2019-02-21 15:36:19 +01:00
|
|
|
accountMap = {};
|
2019-02-21 14:21:16 +01:00
|
|
|
}
|
|
|
|
|
2019-02-21 15:36:19 +01:00
|
|
|
int _theme = prefs.getInt(StorageKeys.theme);
|
2019-02-09 06:29:44 +01:00
|
|
|
print('read theme: $_theme');
|
2019-02-21 14:21:16 +01:00
|
|
|
if (ThemeMap.values.contains(_theme)) {
|
2019-02-07 07:35:19 +01:00
|
|
|
theme = _theme;
|
|
|
|
} else if (Platform.isIOS) {
|
|
|
|
theme = ThemeMap.cupertino;
|
2019-01-31 07:20:48 +01:00
|
|
|
}
|
2019-02-07 07:35:19 +01:00
|
|
|
|
|
|
|
setState(() {
|
|
|
|
ready = true;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-02-21 15:36:19 +01:00
|
|
|
void setActiveAccount(String platform, String domain, String login) {
|
2019-02-07 07:35:19 +01:00
|
|
|
setState(() {
|
2019-02-21 15:36:19 +01:00
|
|
|
activePlatform = platform;
|
|
|
|
activeDomain = domain;
|
2019-02-21 11:43:41 +01:00
|
|
|
activeLogin = login;
|
2019-02-08 16:20:28 +01:00
|
|
|
});
|
2019-02-07 07:35:19 +01:00
|
|
|
}
|
|
|
|
|
2019-02-08 13:52:10 +01:00
|
|
|
// http timeout
|
|
|
|
var _timeoutDuration = Duration(seconds: 10);
|
|
|
|
// var _timeoutDuration = Duration(seconds: 1);
|
|
|
|
|
2019-02-07 07:35:19 +01:00
|
|
|
Future<dynamic> query(String query, [String _token]) async {
|
|
|
|
if (_token == null) {
|
|
|
|
_token = token;
|
|
|
|
}
|
|
|
|
if (_token == null) {
|
|
|
|
throw Exception('token is null');
|
|
|
|
}
|
|
|
|
|
2019-02-08 13:52:10 +01:00
|
|
|
final res = await http
|
|
|
|
.post(prefix + '/graphql',
|
|
|
|
headers: {
|
|
|
|
HttpHeaders.authorizationHeader: 'token $_token',
|
|
|
|
HttpHeaders.contentTypeHeader: 'application/json'
|
|
|
|
},
|
|
|
|
body: json.encode({'query': query}))
|
|
|
|
.timeout(_timeoutDuration);
|
|
|
|
|
2019-03-10 16:34:34 +01:00
|
|
|
// print(res.body);
|
2019-02-07 07:35:19 +01:00
|
|
|
final data = json.decode(res.body);
|
|
|
|
|
|
|
|
if (data['errors'] != null) {
|
2019-03-10 16:34:34 +01:00
|
|
|
throw new Exception(data['errors'][0]['message']);
|
2019-02-07 07:35:19 +01:00
|
|
|
}
|
2019-02-10 12:15:50 +01:00
|
|
|
|
2019-02-07 07:35:19 +01:00
|
|
|
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;
|
|
|
|
}
|
2019-02-08 13:52:10 +01:00
|
|
|
final res = await http
|
|
|
|
.get(prefix + url, headers: headers)
|
|
|
|
.timeout(_timeoutDuration);
|
2019-02-08 16:20:28 +01:00
|
|
|
// print(res.body);
|
2019-02-07 07:35:19 +01:00
|
|
|
final data = json.decode(res.body);
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<dynamic> patchWithCredentials(String url) async {
|
|
|
|
var headers = {HttpHeaders.authorizationHeader: 'token $token'};
|
2019-02-08 13:52:10 +01:00
|
|
|
await http.patch(prefix + url, headers: headers).timeout(_timeoutDuration);
|
|
|
|
|
2019-02-07 07:35:19 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<dynamic> putWithCredentials(String url,
|
|
|
|
{String contentType, String body}) async {
|
|
|
|
var headers = {HttpHeaders.authorizationHeader: 'token $token'};
|
2019-02-08 13:52:10 +01:00
|
|
|
final res = await http
|
|
|
|
.put(prefix + url, headers: headers, body: body ?? {})
|
|
|
|
.timeout(_timeoutDuration);
|
|
|
|
|
2019-02-10 12:15:50 +01:00
|
|
|
// print(res.body);
|
2019-02-07 13:28:48 +01:00
|
|
|
return true;
|
2019-02-07 07:35:19 +01:00
|
|
|
}
|
|
|
|
|
2019-02-10 11:50:40 +01:00
|
|
|
Future<dynamic> deleteWithCredentials(String url) async {
|
|
|
|
var headers = {HttpHeaders.authorizationHeader: 'token $token'};
|
|
|
|
final res = await http
|
|
|
|
.delete(prefix + url, headers: headers)
|
|
|
|
.timeout(_timeoutDuration);
|
2019-02-10 12:15:50 +01:00
|
|
|
// print(res.body);
|
2019-02-10 11:50:40 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2019-03-10 14:53:35 +01:00
|
|
|
void pushRoute({
|
|
|
|
@required BuildContext context,
|
|
|
|
@required WidgetBuilder builder,
|
|
|
|
bool fullscreenDialog = false,
|
|
|
|
}) {
|
|
|
|
switch (theme) {
|
|
|
|
case ThemeMap.cupertino:
|
|
|
|
Navigator.of(context).push(CupertinoPageRoute(
|
|
|
|
builder: builder,
|
|
|
|
fullscreenDialog: fullscreenDialog,
|
|
|
|
));
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
Navigator.of(context).push(MaterialPageRoute(
|
|
|
|
builder: builder,
|
|
|
|
fullscreenDialog: fullscreenDialog,
|
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-07 07:35:19 +01:00
|
|
|
String randomString;
|
|
|
|
|
|
|
|
generateRandomString() {
|
|
|
|
randomString = nanoid();
|
|
|
|
return randomString;
|
2019-01-31 07:20:48 +01:00
|
|
|
}
|
2019-01-30 07:46:18 +01:00
|
|
|
|
|
|
|
@override
|
2019-01-31 07:20:48 +01:00
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return new _InheritedSettingsProvider(
|
|
|
|
data: this,
|
|
|
|
child: widget.child,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _InheritedSettingsProvider extends InheritedWidget {
|
2019-02-21 11:43:41 +01:00
|
|
|
final SettingsProviderState data;
|
2019-01-30 07:46:18 +01:00
|
|
|
|
2019-01-31 07:20:48 +01:00
|
|
|
_InheritedSettingsProvider({
|
|
|
|
Key key,
|
|
|
|
@required this.data,
|
|
|
|
@required Widget child,
|
|
|
|
}) : super(key: key, child: child);
|
|
|
|
|
|
|
|
@override
|
|
|
|
bool updateShouldNotify(_InheritedSettingsProvider old) => true;
|
2019-01-30 07:46:18 +01:00
|
|
|
}
|