2019-02-07 14:35:19 +08:00
|
|
|
import 'dart:convert';
|
|
|
|
import 'dart:async';
|
2021-01-17 22:08:32 +08:00
|
|
|
import 'package:ferry/ferry.dart';
|
|
|
|
import 'package:git_touch/utils/nil_store.dart';
|
2021-01-11 10:22:11 +08:00
|
|
|
// import 'package:in_app_review/in_app_review.dart';
|
2020-10-17 16:47:11 +08:00
|
|
|
import 'package:universal_io/io.dart';
|
2020-02-02 14:08:58 +08:00
|
|
|
import 'package:git_touch/models/bitbucket.dart';
|
2020-01-29 17:33:54 +08:00
|
|
|
import 'package:git_touch/models/gitea.dart';
|
2020-10-14 00:43:11 +08:00
|
|
|
import 'package:git_touch/models/gitee.dart';
|
2020-02-07 21:44:27 +08:00
|
|
|
import 'package:github/github.dart';
|
2019-12-06 21:51:33 +08:00
|
|
|
import 'package:gql_http_link/gql_http_link.dart';
|
2019-10-13 10:01:29 +07:00
|
|
|
import 'package:fimber/fimber.dart';
|
2019-02-07 14:35:19 +08:00
|
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import 'package:uni_links/uni_links.dart';
|
|
|
|
import 'package:nanoid/nanoid.dart';
|
2019-02-12 00:22:58 +08:00
|
|
|
import 'package:url_launcher/url_launcher.dart';
|
2019-01-31 14:20:48 +08:00
|
|
|
import 'package:flutter/material.dart';
|
2019-03-10 21:53:35 +08:00
|
|
|
import 'package:flutter/cupertino.dart';
|
2019-02-07 14:35:19 +08:00
|
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
2019-03-10 16:09:26 +08:00
|
|
|
import '../utils/utils.dart';
|
2019-09-08 20:07:35 +08:00
|
|
|
import 'account.dart';
|
2019-12-30 20:50:31 +08:00
|
|
|
import 'gitlab.dart';
|
2021-01-23 19:38:05 +05:30
|
|
|
import 'gogs.dart';
|
2019-02-21 18:43:41 +08:00
|
|
|
|
2020-01-27 13:24:01 +08:00
|
|
|
const clientId = 'df930d7d2e219f26142a';
|
|
|
|
|
2019-02-21 21:21:16 +08:00
|
|
|
class PlatformType {
|
2019-02-21 22:36:19 +08:00
|
|
|
static const github = 'github';
|
|
|
|
static const gitlab = 'gitlab';
|
2020-02-02 14:08:58 +08:00
|
|
|
static const bitbucket = 'bitbucket';
|
2020-01-29 17:33:54 +08:00
|
|
|
static const gitea = 'gitea';
|
2020-10-14 00:43:11 +08:00
|
|
|
static const gitee = 'gitee';
|
2021-01-23 19:38:05 +05:30
|
|
|
static const gogs = 'gogs';
|
2019-02-21 22:36:19 +08:00
|
|
|
}
|
|
|
|
|
2020-01-29 12:32:35 +08:00
|
|
|
class DataWithPage<T> {
|
|
|
|
T data;
|
|
|
|
int cursor;
|
|
|
|
bool hasMore;
|
2020-02-01 11:40:41 +08:00
|
|
|
int total;
|
2020-02-02 16:40:12 +08:00
|
|
|
DataWithPage({
|
2021-05-16 15:16:35 +08:00
|
|
|
/*required*/ required this.data,
|
|
|
|
/*required*/ required this.cursor,
|
|
|
|
/*required*/ required this.hasMore,
|
|
|
|
required this.total,
|
2020-02-02 16:40:12 +08:00
|
|
|
});
|
2020-01-29 12:32:35 +08:00
|
|
|
}
|
|
|
|
|
2020-02-02 17:30:48 +08:00
|
|
|
class BbPagePayload<T> {
|
|
|
|
T data;
|
2021-05-16 15:16:35 +08:00
|
|
|
String? cursor;
|
2020-02-02 17:30:48 +08:00
|
|
|
bool hasMore;
|
|
|
|
int total;
|
|
|
|
BbPagePayload({
|
2021-05-16 15:16:35 +08:00
|
|
|
required this.data,
|
|
|
|
required this.cursor,
|
|
|
|
required this.hasMore,
|
|
|
|
required this.total,
|
2020-02-02 17:30:48 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-09-27 20:52:38 +08:00
|
|
|
class AuthModel with ChangeNotifier {
|
2019-09-26 22:14:14 +08:00
|
|
|
static const _apiPrefix = 'https://api.github.com';
|
2019-02-21 21:21:16 +08:00
|
|
|
|
2021-01-11 10:22:11 +08:00
|
|
|
// static final inAppReview = InAppReview.instance;
|
2020-12-12 23:38:10 +08:00
|
|
|
var hasRequestedReview = false;
|
|
|
|
|
2021-05-16 15:16:35 +08:00
|
|
|
List<Account>? _accounts;
|
|
|
|
int? activeAccountIndex;
|
|
|
|
late StreamSubscription<Uri?> _sub;
|
2019-02-10 13:17:25 +08:00
|
|
|
bool loading = false;
|
2019-02-07 14:35:19 +08:00
|
|
|
|
2021-05-16 15:16:35 +08:00
|
|
|
List<Account>? get accounts => _accounts;
|
|
|
|
Account? get activeAccount {
|
2019-09-26 22:14:14 +08:00
|
|
|
if (activeAccountIndex == null || _accounts == null) return null;
|
2021-05-16 15:16:35 +08:00
|
|
|
return _accounts![activeAccountIndex!];
|
2019-02-07 14:35:19 +08:00
|
|
|
}
|
2019-01-30 14:46:18 +08:00
|
|
|
|
2021-05-16 15:16:35 +08:00
|
|
|
String get token => activeAccount!.token;
|
2019-09-26 22:14:14 +08:00
|
|
|
|
2019-09-27 20:52:38 +08:00
|
|
|
_addAccount(Account account) async {
|
2021-05-16 15:16:35 +08:00
|
|
|
_accounts = [...accounts!, account];
|
2019-09-26 22:28:40 +08:00
|
|
|
// Save
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
await prefs.setString(StorageKeys.accounts, json.encode(_accounts));
|
|
|
|
}
|
|
|
|
|
2020-02-01 18:30:32 +08:00
|
|
|
removeAccount(int index) async {
|
|
|
|
if (activeAccountIndex == index) {
|
|
|
|
activeAccountIndex = null;
|
|
|
|
}
|
2021-05-16 15:16:35 +08:00
|
|
|
_accounts!.removeAt(index);
|
2020-02-01 18:30:32 +08:00
|
|
|
// Save
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
await prefs.setString(StorageKeys.accounts, json.encode(_accounts));
|
|
|
|
notifyListeners();
|
|
|
|
}
|
|
|
|
|
2019-02-07 14:35:19 +08:00
|
|
|
// https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#web-application-flow
|
2021-05-16 15:16:35 +08:00
|
|
|
Future<void> _onSchemeDetected(Uri? uri) async {
|
2019-03-13 21:31:36 +08:00
|
|
|
await closeWebView();
|
2019-02-12 00:22:58 +08:00
|
|
|
|
2019-09-08 20:07:35 +08:00
|
|
|
loading = true;
|
|
|
|
notifyListeners();
|
2019-02-10 13:17:25 +08:00
|
|
|
|
2019-09-26 22:14:14 +08:00
|
|
|
// Get token by code
|
|
|
|
final res = await http.post(
|
2021-05-16 12:09:27 +08:00
|
|
|
Uri.parse('https://git-touch-oauth.vercel.app/api/token'),
|
2019-02-07 14:35:19 +08:00
|
|
|
headers: {
|
|
|
|
HttpHeaders.acceptHeader: 'application/json',
|
|
|
|
HttpHeaders.contentTypeHeader: 'application/json',
|
|
|
|
},
|
|
|
|
body: json.encode({
|
|
|
|
'client_id': clientId,
|
2021-05-16 15:16:35 +08:00
|
|
|
'code': uri!.queryParameters['code'],
|
2019-09-02 21:52:32 +08:00
|
|
|
'state': _oauthState,
|
2019-02-07 14:35:19 +08:00
|
|
|
}),
|
|
|
|
);
|
2019-09-26 22:14:14 +08:00
|
|
|
final token = json.decode(res.body)['access_token'] as String;
|
2019-10-03 12:55:17 +08:00
|
|
|
await loginWithToken(token);
|
2019-02-21 18:43:41 +08:00
|
|
|
}
|
2019-02-07 14:35:19 +08:00
|
|
|
|
2019-10-03 12:55:17 +08:00
|
|
|
Future<void> loginWithToken(String token) async {
|
2020-02-01 17:00:30 +08:00
|
|
|
try {
|
|
|
|
final queryData = await query('''
|
2019-02-07 14:35:19 +08:00
|
|
|
{
|
|
|
|
viewer {
|
|
|
|
login
|
|
|
|
avatarUrl
|
|
|
|
}
|
|
|
|
}
|
2019-02-21 18:43:41 +08:00
|
|
|
''', token);
|
2019-02-07 14:35:19 +08:00
|
|
|
|
2020-02-01 17:00:30 +08:00
|
|
|
await _addAccount(Account(
|
|
|
|
platform: PlatformType.github,
|
|
|
|
domain: 'https://github.com',
|
|
|
|
token: token,
|
|
|
|
login: queryData['viewer']['login'] as String,
|
|
|
|
avatarUrl: queryData['viewer']['avatarUrl'] as String,
|
|
|
|
));
|
|
|
|
} finally {
|
|
|
|
loading = false;
|
|
|
|
notifyListeners();
|
|
|
|
}
|
2019-02-07 14:35:19 +08:00
|
|
|
}
|
|
|
|
|
2019-02-21 21:21:16 +08:00
|
|
|
Future<void> loginToGitlab(String domain, String token) async {
|
2020-02-06 14:38:43 +08:00
|
|
|
domain = domain.trim();
|
|
|
|
token = token.trim();
|
2020-02-01 17:00:30 +08:00
|
|
|
loading = true;
|
|
|
|
notifyListeners();
|
2019-02-21 21:21:16 +08:00
|
|
|
try {
|
2021-05-16 12:09:27 +08:00
|
|
|
final res = await http.get(Uri.parse('$domain/api/v4/user'),
|
|
|
|
headers: {'Private-Token': token});
|
2019-09-26 22:14:14 +08:00
|
|
|
final info = json.decode(res.body);
|
2019-02-21 21:21:16 +08:00
|
|
|
if (info['message'] != null) {
|
|
|
|
throw info['message'];
|
|
|
|
}
|
2020-05-09 12:13:56 +05:30
|
|
|
if (info['error'] != null) {
|
|
|
|
throw info['error'] +
|
|
|
|
'. ' +
|
|
|
|
(info['error_description'] != null
|
|
|
|
? info['error_description']
|
|
|
|
: '');
|
|
|
|
}
|
2019-12-30 20:50:31 +08:00
|
|
|
final user = GitlabUser.fromJson(info);
|
2019-09-27 20:52:38 +08:00
|
|
|
await _addAccount(Account(
|
2019-09-26 22:14:14 +08:00
|
|
|
platform: PlatformType.gitlab,
|
|
|
|
domain: domain,
|
|
|
|
token: token,
|
2021-05-16 15:16:35 +08:00
|
|
|
login: user.username!,
|
|
|
|
avatarUrl: user.avatarUrl!,
|
2019-12-30 20:50:31 +08:00
|
|
|
gitlabId: user.id,
|
2019-09-26 22:28:40 +08:00
|
|
|
));
|
2019-02-21 21:21:16 +08:00
|
|
|
} finally {
|
2019-09-08 20:07:35 +08:00
|
|
|
loading = false;
|
|
|
|
notifyListeners();
|
2019-02-21 21:21:16 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-11 23:37:29 +08:00
|
|
|
Future<String> fetchWithGitlabToken(String p) async {
|
2021-05-16 12:09:27 +08:00
|
|
|
final res = await http.get(Uri.parse(p), headers: {'Private-Token': token});
|
2019-12-11 23:37:29 +08:00
|
|
|
return res.body;
|
|
|
|
}
|
|
|
|
|
2020-10-30 14:36:40 +08:00
|
|
|
Future fetchGitlab(String p,
|
|
|
|
{isPost = false, Map<String, dynamic> body = const {}}) async {
|
|
|
|
http.Response res;
|
|
|
|
if (isPost) {
|
|
|
|
res = await http.post(
|
2021-05-16 15:16:35 +08:00
|
|
|
Uri.parse('${activeAccount!.domain}/api/v4$p'),
|
2020-10-30 14:36:40 +08:00
|
|
|
headers: {
|
|
|
|
'Private-Token': token,
|
|
|
|
HttpHeaders.contentTypeHeader: 'application/json'
|
|
|
|
},
|
|
|
|
body: jsonEncode(body),
|
|
|
|
);
|
|
|
|
} else {
|
2021-05-16 15:16:35 +08:00
|
|
|
res = await http.get(Uri.parse('${activeAccount!.domain}/api/v4$p'),
|
2020-10-30 14:36:40 +08:00
|
|
|
headers: {'Private-Token': token});
|
|
|
|
}
|
2019-11-02 01:12:17 +08:00
|
|
|
final info = json.decode(utf8.decode(res.bodyBytes));
|
2020-01-30 12:53:07 +08:00
|
|
|
if (info is Map && info['message'] != null) throw info['message'];
|
2019-11-02 01:12:17 +08:00
|
|
|
return info;
|
|
|
|
}
|
|
|
|
|
2020-01-29 12:32:35 +08:00
|
|
|
Future<DataWithPage> fetchGitlabWithPage(String p) async {
|
2021-05-16 15:16:35 +08:00
|
|
|
final res = await http.get(Uri.parse('${activeAccount!.domain}/api/v4$p'),
|
2020-01-29 12:32:35 +08:00
|
|
|
headers: {'Private-Token': token});
|
|
|
|
final next = int.tryParse(
|
2021-05-16 15:16:35 +08:00
|
|
|
res.headers['X-Next-Pages'] ?? res.headers['x-next-page'] ?? '')!;
|
2020-01-29 12:32:35 +08:00
|
|
|
final info = json.decode(utf8.decode(res.bodyBytes));
|
2020-01-30 12:53:07 +08:00
|
|
|
if (info is Map && info['message'] != null) throw info['message'];
|
2020-02-01 11:40:41 +08:00
|
|
|
return DataWithPage(
|
|
|
|
data: info,
|
|
|
|
cursor: next,
|
|
|
|
hasMore: next != null,
|
|
|
|
total:
|
2021-05-16 15:16:35 +08:00
|
|
|
int.tryParse(res.headers['X-Total'] ?? res.headers['x-total'] ?? '')!,
|
2020-02-01 11:40:41 +08:00
|
|
|
);
|
2020-01-29 12:32:35 +08:00
|
|
|
}
|
|
|
|
|
2020-01-29 17:33:54 +08:00
|
|
|
Future loginToGitea(String domain, String token) async {
|
2020-02-06 14:38:43 +08:00
|
|
|
domain = domain.trim();
|
|
|
|
token = token.trim();
|
2020-01-29 17:33:54 +08:00
|
|
|
try {
|
|
|
|
loading = true;
|
|
|
|
notifyListeners();
|
2021-05-16 12:09:27 +08:00
|
|
|
final res = await http.get(Uri.parse('$domain/api/v1/user'),
|
2020-01-29 17:33:54 +08:00
|
|
|
headers: {'Authorization': 'token $token'});
|
|
|
|
final info = json.decode(res.body);
|
|
|
|
if (info['message'] != null) {
|
|
|
|
throw info['message'];
|
|
|
|
}
|
|
|
|
final user = GiteaUser.fromJson(info);
|
|
|
|
|
|
|
|
await _addAccount(Account(
|
|
|
|
platform: PlatformType.gitea,
|
|
|
|
domain: domain,
|
|
|
|
token: token,
|
2021-05-16 15:16:35 +08:00
|
|
|
login: user.login!,
|
|
|
|
avatarUrl: user.avatarUrl!,
|
2020-01-29 17:33:54 +08:00
|
|
|
));
|
|
|
|
} finally {
|
|
|
|
loading = false;
|
|
|
|
notifyListeners();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-10 11:50:03 +05:30
|
|
|
Future fetchGitea(
|
|
|
|
String p, {
|
|
|
|
requestType = 'GET',
|
|
|
|
Map<String, dynamic> body = const {},
|
|
|
|
}) async {
|
2021-05-16 15:16:35 +08:00
|
|
|
late http.Response res;
|
2021-01-10 11:50:03 +05:30
|
|
|
Map<String, String> headers = {
|
|
|
|
'Authorization': 'token $token',
|
|
|
|
HttpHeaders.contentTypeHeader: 'application/json'
|
|
|
|
};
|
|
|
|
switch (requestType) {
|
|
|
|
case 'DELETE':
|
|
|
|
{
|
|
|
|
await http.delete(
|
2021-05-16 15:16:35 +08:00
|
|
|
Uri.parse('${activeAccount!.domain}/api/v1$p'),
|
2021-01-10 11:50:03 +05:30
|
|
|
headers: headers,
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'POST':
|
|
|
|
{
|
|
|
|
res = await http.post(
|
2021-05-16 15:16:35 +08:00
|
|
|
Uri.parse('${activeAccount!.domain}/api/v1$p'),
|
2021-01-10 11:50:03 +05:30
|
|
|
headers: headers,
|
|
|
|
body: jsonEncode(body),
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'PATCH':
|
|
|
|
{
|
|
|
|
res = await http.patch(
|
2021-05-16 15:16:35 +08:00
|
|
|
Uri.parse('${activeAccount!.domain}/api/v1$p'),
|
2021-01-10 11:50:03 +05:30
|
|
|
headers: headers,
|
|
|
|
body: jsonEncode(body),
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
{
|
2021-05-16 15:16:35 +08:00
|
|
|
res = await http.get(Uri.parse('${activeAccount!.domain}/api/v1$p'),
|
2021-01-10 11:50:03 +05:30
|
|
|
headers: headers);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (requestType != 'DELETE') {
|
|
|
|
final info = json.decode(utf8.decode(res.bodyBytes));
|
|
|
|
return info;
|
|
|
|
}
|
|
|
|
return;
|
2019-12-04 22:02:22 +08:00
|
|
|
}
|
|
|
|
|
2020-10-06 11:54:35 +08:00
|
|
|
Future<DataWithPage> fetchGiteaWithPage(String path,
|
2021-05-16 15:16:35 +08:00
|
|
|
{int? page, int? limit}) async {
|
2021-01-23 19:38:05 +05:30
|
|
|
page = page ?? 1;
|
|
|
|
limit = limit ?? pageSize;
|
|
|
|
|
2021-05-16 15:16:35 +08:00
|
|
|
var uri = Uri.parse('${activeAccount!.domain}/api/v1$path');
|
2021-01-23 19:38:05 +05:30
|
|
|
uri = uri.replace(
|
|
|
|
queryParameters: {
|
|
|
|
'page': page.toString(),
|
|
|
|
'limit': limit.toString(),
|
|
|
|
...uri.queryParameters,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
final res = await http.get(uri, headers: {'Authorization': 'token $token'});
|
|
|
|
final info = json.decode(utf8.decode(res.bodyBytes));
|
|
|
|
|
|
|
|
return DataWithPage(
|
|
|
|
data: info,
|
|
|
|
cursor: page + 1,
|
|
|
|
hasMore: info is List && info.length > 0,
|
2021-05-16 15:16:35 +08:00
|
|
|
total: int.tryParse(res.headers['x-total-count'] ?? '')!,
|
2021-01-23 19:38:05 +05:30
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future loginToGogs(String domain, String token) async {
|
|
|
|
domain = domain.trim();
|
|
|
|
token = token.trim();
|
|
|
|
try {
|
|
|
|
loading = true;
|
|
|
|
notifyListeners();
|
2021-05-16 12:09:27 +08:00
|
|
|
final res = await http.get(Uri.parse('$domain/api/v1/user'),
|
2021-01-23 19:38:05 +05:30
|
|
|
headers: {'Authorization': 'token $token'});
|
|
|
|
final info = json.decode(res.body);
|
|
|
|
if (info['message'] != null) {
|
|
|
|
throw info['message'];
|
|
|
|
}
|
|
|
|
final user = GogsUser.fromJson(info);
|
|
|
|
|
|
|
|
await _addAccount(Account(
|
|
|
|
platform: PlatformType.gogs,
|
|
|
|
domain: domain,
|
|
|
|
token: token,
|
2021-05-16 15:16:35 +08:00
|
|
|
login: user.username!,
|
|
|
|
avatarUrl: user.avatarUrl!,
|
2021-01-23 19:38:05 +05:30
|
|
|
));
|
|
|
|
} finally {
|
|
|
|
loading = false;
|
|
|
|
notifyListeners();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: refactor
|
|
|
|
Future fetchGogs(
|
|
|
|
String p, {
|
|
|
|
requestType = 'GET',
|
|
|
|
Map<String, dynamic> body = const {},
|
|
|
|
}) async {
|
2021-05-16 15:16:35 +08:00
|
|
|
late http.Response res;
|
2021-01-23 19:38:05 +05:30
|
|
|
Map<String, String> headers = {
|
|
|
|
'Authorization': 'token $token',
|
|
|
|
HttpHeaders.contentTypeHeader: 'application/json'
|
|
|
|
};
|
|
|
|
switch (requestType) {
|
|
|
|
case 'DELETE':
|
|
|
|
{
|
|
|
|
await http.delete(
|
2021-05-16 15:16:35 +08:00
|
|
|
Uri.parse('${activeAccount!.domain}/api/v1$p'),
|
2021-01-23 19:38:05 +05:30
|
|
|
headers: headers,
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'POST':
|
|
|
|
{
|
|
|
|
res = await http.post(
|
2021-05-16 15:16:35 +08:00
|
|
|
Uri.parse('${activeAccount!.domain}/api/v1$p'),
|
2021-01-23 19:38:05 +05:30
|
|
|
headers: headers,
|
|
|
|
body: jsonEncode(body),
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'PATCH':
|
|
|
|
{
|
|
|
|
res = await http.patch(
|
2021-05-16 15:16:35 +08:00
|
|
|
Uri.parse('${activeAccount!.domain}/api/v1$p'),
|
2021-01-23 19:38:05 +05:30
|
|
|
headers: headers,
|
|
|
|
body: jsonEncode(body),
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
{
|
2021-05-16 15:16:35 +08:00
|
|
|
res = await http.get(Uri.parse('${activeAccount!.domain}/api/v1$p'),
|
2021-01-23 19:38:05 +05:30
|
|
|
headers: headers);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (requestType != 'DELETE') {
|
|
|
|
final info = json.decode(utf8.decode(res.bodyBytes));
|
|
|
|
return info;
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<DataWithPage> fetchGogsWithPage(String path,
|
2021-05-16 15:16:35 +08:00
|
|
|
{int? page, int? limit}) async {
|
2020-10-06 11:54:35 +08:00
|
|
|
page = page ?? 1;
|
|
|
|
limit = limit ?? pageSize;
|
|
|
|
|
2021-05-16 15:16:35 +08:00
|
|
|
var uri = Uri.parse('${activeAccount!.domain}/api/v1$path');
|
2020-10-06 12:12:57 +08:00
|
|
|
uri = uri.replace(
|
|
|
|
queryParameters: {
|
|
|
|
'page': page.toString(),
|
|
|
|
'limit': limit.toString(),
|
|
|
|
...uri.queryParameters,
|
|
|
|
},
|
2020-10-06 11:54:35 +08:00
|
|
|
);
|
|
|
|
final res = await http.get(uri, headers: {'Authorization': 'token $token'});
|
2020-02-01 15:21:42 +08:00
|
|
|
final info = json.decode(utf8.decode(res.bodyBytes));
|
2020-10-06 11:54:35 +08:00
|
|
|
|
2020-02-01 15:21:42 +08:00
|
|
|
return DataWithPage(
|
|
|
|
data: info,
|
2020-10-06 11:54:35 +08:00
|
|
|
cursor: page + 1,
|
|
|
|
hasMore: info is List && info.length > 0,
|
2021-05-16 15:16:35 +08:00
|
|
|
total: int.tryParse(res.headers['x-total-count'] ?? '')!,
|
2020-02-01 15:21:42 +08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-01-06 10:22:58 +05:30
|
|
|
Future fetchGitee(
|
|
|
|
String p, {
|
2021-01-09 08:53:52 +05:30
|
|
|
requestType = 'GET',
|
2021-01-06 10:22:58 +05:30
|
|
|
Map<String, dynamic> body = const {},
|
|
|
|
}) async {
|
|
|
|
http.Response res;
|
2021-01-09 08:53:52 +05:30
|
|
|
Map<String, String> headers = {
|
|
|
|
'Authorization': 'token $token',
|
|
|
|
HttpHeaders.contentTypeHeader: 'application/json'
|
|
|
|
};
|
|
|
|
switch (requestType) {
|
|
|
|
case 'DELETE':
|
|
|
|
{
|
|
|
|
await http.delete(
|
2021-05-16 15:16:35 +08:00
|
|
|
Uri.parse('${activeAccount!.domain}/api/v5$p'),
|
2021-01-09 08:53:52 +05:30
|
|
|
headers: headers,
|
|
|
|
);
|
2021-01-11 20:01:54 +05:30
|
|
|
return;
|
|
|
|
}
|
|
|
|
case 'PUT':
|
|
|
|
{
|
|
|
|
await http.put(
|
2021-05-16 15:16:35 +08:00
|
|
|
Uri.parse('${activeAccount!.domain}/api/v5$p'),
|
2021-01-11 20:01:54 +05:30
|
|
|
headers: headers,
|
|
|
|
);
|
|
|
|
return;
|
2021-01-09 08:53:52 +05:30
|
|
|
}
|
|
|
|
case 'POST':
|
|
|
|
{
|
|
|
|
res = await http.post(
|
2021-05-16 15:16:35 +08:00
|
|
|
Uri.parse('${activeAccount!.domain}/api/v5$p'),
|
2021-01-09 08:53:52 +05:30
|
|
|
headers: headers,
|
|
|
|
body: jsonEncode(body),
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'PATCH':
|
|
|
|
{
|
|
|
|
res = await http.patch(
|
2021-05-16 15:16:35 +08:00
|
|
|
Uri.parse('${activeAccount!.domain}/api/v5$p'),
|
2021-01-09 08:53:52 +05:30
|
|
|
headers: headers,
|
|
|
|
body: jsonEncode(body),
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
}
|
2021-01-11 20:01:54 +05:30
|
|
|
case 'NO CONTENT':
|
|
|
|
{
|
2021-05-16 15:16:35 +08:00
|
|
|
res = await http.get(Uri.parse('${activeAccount!.domain}/api/v5$p'),
|
2021-01-11 20:01:54 +05:30
|
|
|
headers: headers);
|
|
|
|
return res;
|
|
|
|
}
|
2021-01-09 08:53:52 +05:30
|
|
|
default:
|
|
|
|
{
|
2021-05-16 15:16:35 +08:00
|
|
|
res = await http.get(Uri.parse('${activeAccount!.domain}/api/v5$p'),
|
2021-01-09 08:53:52 +05:30
|
|
|
headers: headers);
|
|
|
|
break;
|
|
|
|
}
|
2021-01-06 10:22:58 +05:30
|
|
|
}
|
2021-01-11 20:01:54 +05:30
|
|
|
final info = json.decode(utf8.decode(res.bodyBytes));
|
|
|
|
return info;
|
2020-10-17 17:35:08 +08:00
|
|
|
}
|
|
|
|
|
2020-10-17 18:09:36 +08:00
|
|
|
Future<DataWithPage> fetchGiteeWithPage(String path,
|
2021-05-16 15:16:35 +08:00
|
|
|
{int? page, int? limit}) async {
|
2020-10-17 18:09:36 +08:00
|
|
|
page = page ?? 1;
|
|
|
|
limit = limit ?? pageSize;
|
|
|
|
|
2021-05-16 15:16:35 +08:00
|
|
|
var uri = Uri.parse('${activeAccount!.domain}/api/v5$path');
|
2020-10-17 18:09:36 +08:00
|
|
|
uri = uri.replace(
|
|
|
|
queryParameters: {
|
|
|
|
'page': page.toString(),
|
|
|
|
'per_page': limit.toString(),
|
|
|
|
...uri.queryParameters,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
final res = await http.get(uri, headers: {'Authorization': 'token $token'});
|
|
|
|
final info = json.decode(utf8.decode(res.bodyBytes));
|
|
|
|
|
2020-10-17 20:46:47 +08:00
|
|
|
final totalPage = int.tryParse(res.headers['total_page'] ?? '');
|
2021-05-16 15:16:35 +08:00
|
|
|
final totalCount = int.tryParse(res.headers['total_count'] ?? '')!;
|
2020-10-17 20:46:47 +08:00
|
|
|
|
2020-10-17 18:09:36 +08:00
|
|
|
return DataWithPage(
|
|
|
|
data: info,
|
|
|
|
cursor: page + 1,
|
2020-10-17 20:46:47 +08:00
|
|
|
hasMore: totalPage == null ? info.length > limit : totalPage > page,
|
|
|
|
total: totalCount,
|
2020-10-17 18:09:36 +08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-02-02 14:08:58 +08:00
|
|
|
Future loginToBb(String domain, String username, String appPassword) async {
|
2020-02-06 14:38:43 +08:00
|
|
|
domain = domain.trim();
|
|
|
|
username = username.trim();
|
|
|
|
appPassword = appPassword.trim();
|
2020-02-02 14:08:58 +08:00
|
|
|
try {
|
|
|
|
loading = true;
|
|
|
|
notifyListeners();
|
|
|
|
final uri = Uri.parse('$domain/api/2.0/user')
|
|
|
|
.replace(userInfo: '$username:$appPassword');
|
|
|
|
final res = await http.get(uri);
|
|
|
|
if (res.statusCode >= 400) {
|
|
|
|
throw 'status ${res.statusCode}';
|
|
|
|
}
|
|
|
|
final info = json.decode(res.body);
|
|
|
|
final user = BbUser.fromJson(info);
|
|
|
|
await _addAccount(Account(
|
|
|
|
platform: PlatformType.bitbucket,
|
|
|
|
domain: domain,
|
2021-05-16 15:16:35 +08:00
|
|
|
token: user.username!,
|
2020-02-02 14:08:58 +08:00
|
|
|
login: username,
|
2021-05-16 15:16:35 +08:00
|
|
|
avatarUrl: user.avatarUrl!,
|
2020-02-02 14:08:58 +08:00
|
|
|
appPassword: appPassword,
|
2020-05-11 18:27:18 +05:30
|
|
|
accountId: user.accountId,
|
2020-02-02 14:08:58 +08:00
|
|
|
));
|
|
|
|
} finally {
|
|
|
|
loading = false;
|
|
|
|
notifyListeners();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-06 18:47:49 +05:30
|
|
|
Future<http.Response> fetchBb(
|
|
|
|
String p, {
|
|
|
|
isPost = false,
|
|
|
|
Map<String, dynamic> body = const {},
|
|
|
|
}) async {
|
2020-02-02 17:30:48 +08:00
|
|
|
if (p.startsWith('/') && !p.startsWith('/api')) p = '/api/2.0$p';
|
2020-02-02 16:40:12 +08:00
|
|
|
final input = Uri.parse(p);
|
2021-05-16 15:16:35 +08:00
|
|
|
final uri = Uri.parse(activeAccount!.domain).replace(
|
|
|
|
userInfo: '${activeAccount!.login}:${activeAccount!.appPassword}',
|
2020-02-02 16:40:12 +08:00
|
|
|
path: input.path,
|
|
|
|
query: input.query,
|
|
|
|
);
|
2021-01-06 18:47:49 +05:30
|
|
|
if (isPost) {
|
|
|
|
return http.post(
|
|
|
|
uri,
|
|
|
|
headers: {HttpHeaders.contentTypeHeader: 'application/json'},
|
|
|
|
body: jsonEncode(body),
|
|
|
|
);
|
|
|
|
}
|
2020-02-02 19:50:00 +08:00
|
|
|
return http.get(uri);
|
2020-02-02 16:40:12 +08:00
|
|
|
}
|
|
|
|
|
2021-01-06 18:47:49 +05:30
|
|
|
Future fetchBbJson(
|
|
|
|
String p, {
|
|
|
|
isPost = false,
|
|
|
|
Map<String, dynamic> body = const {},
|
|
|
|
}) async {
|
|
|
|
final res = await fetchBb(
|
|
|
|
p,
|
|
|
|
isPost: isPost,
|
|
|
|
body: body,
|
|
|
|
);
|
2020-02-02 19:50:00 +08:00
|
|
|
return json.decode(utf8.decode(res.bodyBytes));
|
|
|
|
}
|
|
|
|
|
2021-05-16 15:16:35 +08:00
|
|
|
Future<BbPagePayload<List?>> fetchBbWithPage(String p) async {
|
2020-02-02 19:50:00 +08:00
|
|
|
final data = await fetchBbJson(p);
|
|
|
|
final v = BbPagination.fromJson(data);
|
2020-02-02 17:30:48 +08:00
|
|
|
return BbPagePayload(
|
|
|
|
cursor: v.next,
|
2021-05-16 15:16:35 +08:00
|
|
|
total: v.size!,
|
2020-02-02 16:40:12 +08:00
|
|
|
data: v.values,
|
|
|
|
hasMore: v.next != null,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-10-14 00:43:11 +08:00
|
|
|
Future loginToGitee(String token) async {
|
|
|
|
token = token.trim();
|
|
|
|
try {
|
|
|
|
loading = true;
|
|
|
|
notifyListeners();
|
2021-05-16 12:09:27 +08:00
|
|
|
final res = await http.get(Uri.parse('https://gitee.com/api/v5/user'),
|
2020-10-14 00:43:11 +08:00
|
|
|
headers: {'Authorization': 'token $token'});
|
|
|
|
final info = json.decode(res.body);
|
|
|
|
if (info['message'] != null) {
|
|
|
|
throw info['message'];
|
|
|
|
}
|
|
|
|
final user = GiteeUser.fromJson(info);
|
|
|
|
|
|
|
|
await _addAccount(Account(
|
2020-10-17 17:27:13 +08:00
|
|
|
platform: PlatformType.gitee,
|
2020-10-14 00:43:11 +08:00
|
|
|
domain: 'https://gitee.com',
|
|
|
|
token: token,
|
2021-05-16 15:16:35 +08:00
|
|
|
login: user.login!,
|
|
|
|
avatarUrl: user.avatarUrl!,
|
2020-10-14 00:43:11 +08:00
|
|
|
));
|
|
|
|
} finally {
|
|
|
|
loading = false;
|
|
|
|
notifyListeners();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-05 15:09:54 +08:00
|
|
|
Future<void> init() async {
|
2019-09-26 22:14:14 +08:00
|
|
|
// Listen scheme
|
2021-05-16 13:23:31 +08:00
|
|
|
_sub = uriLinkStream.listen(_onSchemeDetected, onError: (err) {
|
2019-10-13 10:01:29 +07:00
|
|
|
Fimber.e('getUriLinksStream failed', ex: err);
|
2019-09-08 20:07:35 +08:00
|
|
|
});
|
|
|
|
|
2019-09-02 21:52:32 +08:00
|
|
|
var prefs = await SharedPreferences.getInstance();
|
2019-02-07 14:35:19 +08:00
|
|
|
|
2019-09-26 22:14:14 +08:00
|
|
|
// Read accounts
|
2019-02-21 21:21:16 +08:00
|
|
|
try {
|
2021-05-16 15:16:35 +08:00
|
|
|
String? str = prefs.getString(StorageKeys.accounts);
|
2020-02-08 15:00:44 +08:00
|
|
|
// Fimber.d('read accounts: $str');
|
2019-09-26 22:14:14 +08:00
|
|
|
_accounts = (json.decode(str ?? '[]') as List)
|
2019-09-27 20:52:38 +08:00
|
|
|
.map((item) => Account.fromJson(item))
|
2019-09-26 22:14:14 +08:00
|
|
|
.toList();
|
2021-02-14 10:27:34 +08:00
|
|
|
activeAccountIndex = prefs.getInt(StorageKeys.iDefaultAccount);
|
2021-02-05 09:11:55 +05:30
|
|
|
|
2021-02-14 10:27:34 +08:00
|
|
|
if (activeAccount != null) {
|
2021-02-05 09:11:55 +05:30
|
|
|
_activeTab = prefs.getInt(
|
2021-05-16 15:16:35 +08:00
|
|
|
StorageKeys.getDefaultStartTabKey(activeAccount!.platform)) ??
|
2021-02-05 09:11:55 +05:30
|
|
|
0;
|
2021-02-14 10:27:34 +08:00
|
|
|
}
|
2019-02-21 21:21:16 +08:00
|
|
|
} catch (err) {
|
2019-10-13 10:01:29 +07:00
|
|
|
Fimber.e('prefs getAccount failed', ex: err);
|
2019-09-26 22:14:14 +08:00
|
|
|
_accounts = [];
|
2019-02-21 21:21:16 +08:00
|
|
|
}
|
|
|
|
|
2019-09-08 20:07:35 +08:00
|
|
|
notifyListeners();
|
2019-02-07 14:35:19 +08:00
|
|
|
}
|
|
|
|
|
2019-10-03 12:24:09 +08:00
|
|
|
@override
|
|
|
|
void dispose() {
|
|
|
|
_sub.cancel();
|
|
|
|
super.dispose();
|
|
|
|
}
|
|
|
|
|
2021-02-05 09:11:55 +05:30
|
|
|
Future<void> setDefaultAccount(int v) async {
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
2021-02-14 10:27:34 +08:00
|
|
|
await prefs.setInt(StorageKeys.iDefaultAccount, v);
|
2021-02-05 09:11:55 +05:30
|
|
|
Fimber.d('write default account: $v');
|
|
|
|
notifyListeners();
|
|
|
|
}
|
|
|
|
|
2020-02-01 14:44:15 +08:00
|
|
|
var rootKey = UniqueKey();
|
2021-02-14 21:23:15 +08:00
|
|
|
reloadApp() {
|
|
|
|
rootKey = UniqueKey();
|
|
|
|
notifyListeners();
|
|
|
|
}
|
|
|
|
|
2020-05-01 18:05:49 +08:00
|
|
|
setActiveAccountAndReload(int index) async {
|
2020-02-01 14:44:15 +08:00
|
|
|
// https://stackoverflow.com/a/50116077
|
|
|
|
rootKey = UniqueKey();
|
2019-09-26 22:14:14 +08:00
|
|
|
activeAccountIndex = index;
|
2021-05-16 15:16:35 +08:00
|
|
|
setDefaultAccount(activeAccountIndex!);
|
2020-05-01 18:05:49 +08:00
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
_activeTab = prefs.getInt(
|
2021-05-16 15:16:35 +08:00
|
|
|
StorageKeys.getDefaultStartTabKey(activeAccount!.platform)) ??
|
2020-05-01 18:05:49 +08:00
|
|
|
0;
|
2020-02-07 21:44:27 +08:00
|
|
|
_ghClient = null;
|
2020-01-14 14:46:36 +08:00
|
|
|
_gqlClient = null;
|
2019-09-08 20:07:35 +08:00
|
|
|
notifyListeners();
|
2020-12-12 23:38:10 +08:00
|
|
|
|
2020-12-13 13:08:17 +08:00
|
|
|
// TODO: strategy
|
2020-12-12 23:38:10 +08:00
|
|
|
// waiting for 1min to request review
|
2020-12-13 13:08:17 +08:00
|
|
|
// if (!hasRequestedReview) {
|
|
|
|
// hasRequestedReview = true;
|
|
|
|
// Timer(Duration(minutes: 1), () async {
|
|
|
|
// if (await inAppReview.isAvailable()) {
|
|
|
|
// inAppReview.requestReview();
|
|
|
|
// }
|
|
|
|
// });
|
|
|
|
// }
|
2019-02-07 14:35:19 +08:00
|
|
|
}
|
|
|
|
|
2019-02-08 20:52:10 +08:00
|
|
|
// http timeout
|
|
|
|
var _timeoutDuration = Duration(seconds: 10);
|
|
|
|
// var _timeoutDuration = Duration(seconds: 1);
|
|
|
|
|
2021-05-16 15:16:35 +08:00
|
|
|
GitHub? _ghClient;
|
|
|
|
GitHub? get ghClient {
|
2020-02-07 21:44:27 +08:00
|
|
|
if (token == null) return null;
|
|
|
|
if (_ghClient == null) {
|
|
|
|
_ghClient = GitHub(auth: Authentication.withToken(token));
|
|
|
|
}
|
|
|
|
return _ghClient;
|
|
|
|
}
|
|
|
|
|
2021-05-16 15:16:35 +08:00
|
|
|
Client? _gqlClient;
|
|
|
|
Client? get gqlClient {
|
2019-12-06 21:51:33 +08:00
|
|
|
if (token == null) return null;
|
2019-11-06 21:27:37 +08:00
|
|
|
|
2019-12-06 21:51:33 +08:00
|
|
|
if (_gqlClient == null) {
|
2021-01-17 22:08:32 +08:00
|
|
|
_gqlClient = Client(
|
|
|
|
link: HttpLink(
|
2019-12-07 13:18:44 +08:00
|
|
|
_apiPrefix + '/graphql',
|
|
|
|
defaultHeaders: {HttpHeaders.authorizationHeader: 'token $token'},
|
|
|
|
),
|
2021-01-17 22:08:32 +08:00
|
|
|
cache: Cache(store: NilStore()),
|
2019-12-06 21:51:33 +08:00
|
|
|
);
|
|
|
|
}
|
2019-11-06 21:27:37 +08:00
|
|
|
|
2019-12-06 21:51:33 +08:00
|
|
|
return _gqlClient;
|
|
|
|
}
|
2019-11-06 21:27:37 +08:00
|
|
|
|
2021-05-16 15:16:35 +08:00
|
|
|
Future<dynamic> query(String query, [String? _token]) async {
|
2019-02-07 14:35:19 +08:00
|
|
|
if (_token == null) {
|
|
|
|
_token = token;
|
|
|
|
}
|
|
|
|
if (_token == null) {
|
2019-09-26 22:14:14 +08:00
|
|
|
throw 'token is null';
|
2019-02-07 14:35:19 +08:00
|
|
|
}
|
|
|
|
|
2019-02-08 20:52:10 +08:00
|
|
|
final res = await http
|
2021-05-16 12:09:27 +08:00
|
|
|
.post(Uri.parse(_apiPrefix + '/graphql'),
|
2019-02-08 20:52:10 +08:00
|
|
|
headers: {
|
|
|
|
HttpHeaders.authorizationHeader: 'token $_token',
|
|
|
|
HttpHeaders.contentTypeHeader: 'application/json'
|
|
|
|
},
|
|
|
|
body: json.encode({'query': query}))
|
|
|
|
.timeout(_timeoutDuration);
|
|
|
|
|
2019-10-13 10:01:29 +07:00
|
|
|
// Fimber.d(res.body);
|
2019-02-07 14:35:19 +08:00
|
|
|
final data = json.decode(res.body);
|
|
|
|
|
|
|
|
if (data['errors'] != null) {
|
2019-09-26 22:14:14 +08:00
|
|
|
throw data['errors'][0]['message'];
|
2019-02-07 14:35:19 +08:00
|
|
|
}
|
2019-02-10 19:15:50 +08:00
|
|
|
|
2019-02-07 14:35:19 +08:00
|
|
|
return data['data'];
|
|
|
|
}
|
|
|
|
|
2021-05-16 15:16:35 +08:00
|
|
|
String? _oauthState;
|
2021-01-30 16:18:31 +08:00
|
|
|
void redirectToGithubOauth([publicOnly = false]) {
|
2019-09-02 21:52:32 +08:00
|
|
|
_oauthState = nanoid();
|
2021-01-30 16:18:31 +08:00
|
|
|
final repoScope = publicOnly ? 'public_repo' : 'repo';
|
|
|
|
final scope = Uri.encodeComponent(
|
|
|
|
['user', repoScope, 'read:org', 'notifications'].join(','));
|
2019-09-29 13:32:53 +08:00
|
|
|
launchUrl(
|
2019-09-22 01:02:14 +08:00
|
|
|
'https://github.com/login/oauth/authorize?client_id=$clientId&redirect_uri=gittouch://login&scope=$scope&state=$_oauthState',
|
2019-09-02 21:52:32 +08:00
|
|
|
);
|
2019-01-31 14:20:48 +08:00
|
|
|
}
|
2020-05-01 18:05:49 +08:00
|
|
|
|
|
|
|
int _activeTab = 0;
|
|
|
|
int get activeTab => _activeTab;
|
|
|
|
|
|
|
|
Future<void> setActiveTab(int v) async {
|
|
|
|
_activeTab = v;
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
await prefs.setInt(
|
2021-05-16 15:16:35 +08:00
|
|
|
StorageKeys.getDefaultStartTabKey(activeAccount!.platform), v);
|
|
|
|
Fimber.d('write default start tab for ${activeAccount!.platform}: $v');
|
2020-05-01 18:05:49 +08:00
|
|
|
notifyListeners();
|
|
|
|
}
|
2019-01-30 14:46:18 +08:00
|
|
|
}
|