mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
feat: account manager (#468)
* feat: account manager * remove dep * some fixes * migrate accounts * reimplement clearCookie
This commit is contained in:
committed by
GitHub
parent
94fa0652ac
commit
b15fdfa2ff
193
lib/utils/accounts/account.dart
Normal file
193
lib/utils/accounts/account.dart
Normal file
@@ -0,0 +1,193 @@
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:cookie_jar/cookie_jar.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
abstract class Account {
|
||||
final bool isLogin = false;
|
||||
late final DefaultCookieJar cookieJar;
|
||||
String? accessKey;
|
||||
String? refresh;
|
||||
late final Set<AccountType> type;
|
||||
|
||||
final int mid = 0;
|
||||
late String csrf;
|
||||
final Map<String, String> headers = const {};
|
||||
|
||||
bool activited = false;
|
||||
|
||||
Future<AnonymousAccount> logout();
|
||||
Future<void> onChange();
|
||||
|
||||
Map<String, dynamic>? toJson();
|
||||
}
|
||||
|
||||
@HiveType(typeId: 9)
|
||||
class LoginAccount implements Account {
|
||||
@override
|
||||
final bool isLogin = true;
|
||||
@override
|
||||
@HiveField(0)
|
||||
late final DefaultCookieJar cookieJar;
|
||||
@override
|
||||
@HiveField(1)
|
||||
String? accessKey;
|
||||
@override
|
||||
@HiveField(2)
|
||||
String? refresh;
|
||||
@override
|
||||
@HiveField(3)
|
||||
late final Set<AccountType> type;
|
||||
|
||||
@override
|
||||
late final int mid = int.parse(_midStr);
|
||||
|
||||
@override
|
||||
late final Map<String, String> headers = {
|
||||
'x-bili-mid': _midStr,
|
||||
'x-bili-aurora-eid': Utils.genAuroraEid(mid),
|
||||
};
|
||||
@override
|
||||
late String csrf =
|
||||
cookieJar.domainCookies['bilibili.com']!['/']!['bili_jct']!.cookie.value;
|
||||
|
||||
@override
|
||||
bool activited = false;
|
||||
|
||||
@override
|
||||
Future<AnonymousAccount> logout() async {
|
||||
await Future.wait([cookieJar.deleteAll(), _box.delete(_midStr)]);
|
||||
return AnonymousAccount();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onChange() => _box.put(_midStr, this);
|
||||
|
||||
@override
|
||||
Map<String, dynamic>? toJson() => {
|
||||
'cookies': cookieJar.toJson(),
|
||||
'accessKey': accessKey,
|
||||
'refresh': refresh,
|
||||
'type': type.map((i) => i.index).toList()
|
||||
};
|
||||
|
||||
late final String _midStr = cookieJar
|
||||
.domainCookies['bilibili.com']!['/']!['DedeUserID']!.cookie.value;
|
||||
|
||||
late final Box<LoginAccount> _box = Accounts.account;
|
||||
|
||||
LoginAccount(this.cookieJar, this.accessKey, this.refresh,
|
||||
[Set<AccountType>? type]) {
|
||||
this.type = type ?? {};
|
||||
}
|
||||
|
||||
LoginAccount.fromJson(Map json) {
|
||||
cookieJar = BiliCookieJar.fromJson(json['cookies']);
|
||||
accessKey = json['accessKey'];
|
||||
refresh = json['refresh'];
|
||||
type = (json['type'] as Iterable?)
|
||||
?.map((i) => AccountType.values[i])
|
||||
.toSet() ??
|
||||
{};
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => mid.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) || (other is Account && mid == other.mid);
|
||||
}
|
||||
|
||||
class AnonymousAccount implements Account {
|
||||
@override
|
||||
final bool isLogin = false;
|
||||
@override
|
||||
late final DefaultCookieJar cookieJar;
|
||||
@override
|
||||
String? accessKey;
|
||||
@override
|
||||
String? refresh;
|
||||
@override
|
||||
Set<AccountType> type = {};
|
||||
@override
|
||||
final int mid = 0;
|
||||
@override
|
||||
String csrf = '';
|
||||
@override
|
||||
final Map<String, String> headers = const {};
|
||||
|
||||
@override
|
||||
bool activited = false;
|
||||
|
||||
@override
|
||||
Future<AnonymousAccount> logout() async {
|
||||
await cookieJar.deleteAll();
|
||||
activited = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onChange() async {}
|
||||
|
||||
@override
|
||||
Map<String, dynamic>? toJson() => null;
|
||||
|
||||
static final _instance = AnonymousAccount._();
|
||||
|
||||
AnonymousAccount._() {
|
||||
cookieJar = DefaultCookieJar(ignoreExpires: true);
|
||||
}
|
||||
|
||||
factory AnonymousAccount() => _instance;
|
||||
|
||||
@override
|
||||
int get hashCode => cookieJar.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is Account && cookieJar == other.cookieJar);
|
||||
}
|
||||
|
||||
extension BiliCookie on Cookie {
|
||||
void setBiliDomain([String domain = '.bilibili.com']) {
|
||||
this
|
||||
..domain = domain
|
||||
..httpOnly = false
|
||||
..path = '/';
|
||||
}
|
||||
}
|
||||
|
||||
extension BiliCookieJar on DefaultCookieJar {
|
||||
Map<String, String> toJson() {
|
||||
final cookies = domainCookies['bilibili.com']?['/'] ?? {};
|
||||
return {for (var i in cookies.values) i.cookie.name: i.cookie.value};
|
||||
}
|
||||
|
||||
List<Cookie> toList() =>
|
||||
domainCookies['bilibili.com']?['/']
|
||||
?.entries
|
||||
.map((i) => i.value.cookie)
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
static DefaultCookieJar fromJson(Map json) =>
|
||||
DefaultCookieJar(ignoreExpires: true)
|
||||
..domainCookies['bilibili.com'] = {
|
||||
'/': {
|
||||
for (var i in json.entries)
|
||||
i.key: SerializableCookie(Cookie(i.key, i.value)..setBiliDomain())
|
||||
},
|
||||
};
|
||||
|
||||
static DefaultCookieJar fromList(List cookies) =>
|
||||
DefaultCookieJar(ignoreExpires: true)
|
||||
..domainCookies['bilibili.com'] = {
|
||||
'/': {
|
||||
for (var i in cookies)
|
||||
i['name']!: SerializableCookie(
|
||||
Cookie(i['name']!, i['value']!)..setBiliDomain()),
|
||||
},
|
||||
};
|
||||
}
|
||||
48
lib/utils/accounts/account_adapter.dart
Normal file
48
lib/utils/accounts/account_adapter.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:cookie_jar/cookie_jar.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
import '../storage.dart';
|
||||
import 'account.dart';
|
||||
|
||||
class LoginAccountAdapter extends TypeAdapter<LoginAccount> {
|
||||
@override
|
||||
final int typeId = 9;
|
||||
|
||||
@override
|
||||
LoginAccount read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return LoginAccount(
|
||||
fields[0] as DefaultCookieJar,
|
||||
fields[1] as String?,
|
||||
fields[2] as String?,
|
||||
(fields[3] as List?)?.cast<AccountType>().toSet(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, LoginAccount obj) {
|
||||
writer
|
||||
..writeByte(4)
|
||||
..writeByte(0)
|
||||
..write(obj.cookieJar)
|
||||
..writeByte(1)
|
||||
..write(obj.accessKey)
|
||||
..writeByte(2)
|
||||
..write(obj.refresh)
|
||||
..writeByte(3)
|
||||
..write(obj.type.toList());
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is LoginAccountAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
22
lib/utils/accounts/account_manager/LICENSE
Normal file
22
lib/utils/accounts/account_manager/LICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Wen Du (wendux)
|
||||
Copyright (c) 2022 The CFUG Team
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
90
lib/utils/accounts/account_manager/README.md
Normal file
90
lib/utils/accounts/account_manager/README.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# dio_cookie_manager
|
||||
|
||||
[](https://pub.dev/packages/dio_cookie_manager)
|
||||
|
||||
A cookie manager combines cookie_jar and dio, based on the interceptor algorithm.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Install
|
||||
|
||||
Add the `dio_cookie_manager` package to your
|
||||
[pubspec dependencies](https://pub.dev/packages/dio_cookie_manager/install).
|
||||
|
||||
### Usage
|
||||
|
||||
```dart
|
||||
import 'package:cookie_jar/cookie_jar.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
|
||||
|
||||
void main() async {
|
||||
final dio = Dio();
|
||||
final cookieJar = CookieJar();
|
||||
dio.interceptors.add(CookieManager(cookieJar));
|
||||
// First request, and save cookies (CookieManager do it).
|
||||
await dio.get("https://dart.dev");
|
||||
// Print cookies
|
||||
print(await cookieJar.loadForRequest(Uri.parse("https://dart.dev")));
|
||||
// Second request with the cookies
|
||||
await dio.get('https://dart.dev');
|
||||
}
|
||||
```
|
||||
|
||||
## Cookie Manager
|
||||
|
||||
`CookieManager` Interceptor can help us manage the request/response cookies automatically.
|
||||
`CookieManager` depends on the `cookie_jar` package:
|
||||
|
||||
> The dio_cookie_manager manage API is based on the withdrawn
|
||||
> [cookie_jar](https://github.com/flutterchina/cookie_jar).
|
||||
|
||||
You can create a `CookieJar` or `PersistCookieJar` to manage cookies automatically,
|
||||
and dio use the `CookieJar` by default, which saves the cookies **in RAM**.
|
||||
If you want to persists cookies, you can use the `PersistCookieJar` class, for example:
|
||||
|
||||
```dart
|
||||
dio.interceptors.add(CookieManager(PersistCookieJar()))
|
||||
```
|
||||
|
||||
`PersistCookieJar` persists the cookies in files,
|
||||
so if the application exit, the cookies always exist unless call `delete` explicitly.
|
||||
|
||||
> Note: In flutter, the path passed to `PersistCookieJar` must be valid (exists in phones and with write access).
|
||||
> Use [path_provider](https://pub.dev/packages/path_provider) package to get the right path.
|
||||
|
||||
In flutter:
|
||||
|
||||
```dart
|
||||
Future<void> prepareJar() async {
|
||||
final Directory appDocDir = await getApplicationDocumentsDirectory();
|
||||
final String appDocPath = appDocDir.path;
|
||||
final jar = PersistCookieJar(
|
||||
ignoreExpires: true,
|
||||
storage: FileStorage(appDocPath + "/.cookies/"),
|
||||
);
|
||||
dio.interceptors.add(CookieManager(jar));
|
||||
}
|
||||
```
|
||||
|
||||
## Handling Cookies with redirect requests
|
||||
|
||||
Redirect requests require extra configuration to parse cookies correctly.
|
||||
In shortly:
|
||||
- Set `followRedirects` to `false`.
|
||||
- Allow `statusCode` from `300` to `399` responses predicated as succeed.
|
||||
- Make further requests using the `HttpHeaders.locationHeader`.
|
||||
|
||||
For example:
|
||||
```dart
|
||||
final cookieJar = CookieJar();
|
||||
final dio = Dio()
|
||||
..interceptors.add(CookieManager(cookieJar))
|
||||
..options.followRedirects = false
|
||||
..options.validateStatus =
|
||||
(status) => status != null && status >= 200 && status < 400;
|
||||
final redirected = await dio.get('/redirection');
|
||||
final response = await dio.get(
|
||||
redirected.headers.value(HttpHeaders.locationHeader)!,
|
||||
);
|
||||
```
|
||||
258
lib/utils/accounts/account_manager/account_mgr.dart
Normal file
258
lib/utils/accounts/account_manager/account_mgr.dart
Normal file
@@ -0,0 +1,258 @@
|
||||
// edit from package:dio_cookie_manager
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:PiliPlus/http/api.dart';
|
||||
import 'package:PiliPlus/http/constants.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
|
||||
import '../account.dart';
|
||||
|
||||
final _setCookieReg = RegExp('(?<=)(,)(?=[^;]+?=)');
|
||||
|
||||
class AccountManager extends Interceptor {
|
||||
static final Map<AccountType, Set<String>> apiTypeSet = {
|
||||
AccountType.heartbeat: {
|
||||
Api.videoUrl,
|
||||
Api.videoIntro,
|
||||
Api.relatedList,
|
||||
Api.replyList,
|
||||
Api.replyReplyList,
|
||||
Api.searchSuggest,
|
||||
Api.searchByType,
|
||||
Api.heartBeat,
|
||||
Api.ab2c,
|
||||
Api.bangumiInfo,
|
||||
Api.liveRoomInfo,
|
||||
Api.onlineTotal,
|
||||
Api.dynamicDetail,
|
||||
Api.aiConclusion,
|
||||
Api.getSeasonDetailApi,
|
||||
Api.liveRoomDmToken,
|
||||
Api.liveRoomDmPrefetch,
|
||||
},
|
||||
AccountType.recommend: {
|
||||
Api.recommendListWeb,
|
||||
Api.recommendListApp,
|
||||
Api.feedDislike,
|
||||
Api.feedDislikeCancel,
|
||||
Api.hotList,
|
||||
Api.hotSearchList, // 不同账号搜索结果可能不一样
|
||||
Api.searchDefault,
|
||||
Api.searchSuggest,
|
||||
Api.searchByType
|
||||
},
|
||||
AccountType.video: {Api.videoUrl, Api.bangumiVideoUrl}
|
||||
};
|
||||
|
||||
static final loginApi = {
|
||||
Api.getTVCode,
|
||||
Api.qrcodePoll,
|
||||
Api.getCaptcha,
|
||||
Api.getWebKey,
|
||||
Api.appSmsCode,
|
||||
Api.loginByPwdApi,
|
||||
Api.logInByAppSms,
|
||||
Api.safeCenterGetInfo,
|
||||
Api.preCapture,
|
||||
Api.safeCenterSmsCode,
|
||||
Api.safeCenterSmsVerify,
|
||||
Api.oauth2AccessToken,
|
||||
};
|
||||
|
||||
const AccountManager();
|
||||
|
||||
static String getCookies(List<Cookie> cookies) {
|
||||
// Sort cookies by path (longer path first).
|
||||
cookies.sort((a, b) {
|
||||
if (a.path == null && b.path == null) {
|
||||
return 0;
|
||||
} else if (a.path == null) {
|
||||
return -1;
|
||||
} else if (b.path == null) {
|
||||
return 1;
|
||||
} else {
|
||||
return b.path!.length.compareTo(a.path!.length);
|
||||
}
|
||||
});
|
||||
return cookies.map((cookie) => '${cookie.name}=${cookie.value}').join('; ');
|
||||
}
|
||||
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
final path = options.path;
|
||||
|
||||
if (path.startsWith(GStorage.blockServer)) return handler.next(options);
|
||||
|
||||
final Account account = options.extra['account'] ?? _findAccount(path);
|
||||
|
||||
if (account.isLogin) options.headers.addAll(account.headers);
|
||||
|
||||
// app端不需要管理cookie
|
||||
if (path.startsWith(HttpString.appBaseUrl)) {
|
||||
// debugPrint('is app: ${options.path}');
|
||||
// bytes是grpc响应
|
||||
if (options.responseType != ResponseType.bytes) {
|
||||
final dataPtr = (options.method == 'POST' && options.data is Map
|
||||
? options.data as Map
|
||||
: options.queryParameters)
|
||||
.cast<String, dynamic>();
|
||||
if (dataPtr.isNotEmpty) {
|
||||
if (!account.accessKey.isNullOrEmpty) {
|
||||
dataPtr['access_key'] = account.accessKey!;
|
||||
}
|
||||
dataPtr['ts'] ??=
|
||||
(DateTime.now().millisecondsSinceEpoch ~/ 1000).toString();
|
||||
Utils.appSign(dataPtr);
|
||||
// debugPrint(dataPtr.toString());
|
||||
}
|
||||
}
|
||||
return handler.next(options);
|
||||
} else {
|
||||
account.cookieJar.loadForRequest(options.uri).then((cookies) {
|
||||
final previousCookies =
|
||||
options.headers[HttpHeaders.cookieHeader] as String?;
|
||||
final newCookies = getCookies([
|
||||
...?previousCookies
|
||||
?.split(';')
|
||||
.where((e) => e.isNotEmpty)
|
||||
.map((c) => Cookie.fromSetCookieValue(c)),
|
||||
...cookies,
|
||||
]);
|
||||
options.headers[HttpHeaders.cookieHeader] =
|
||||
newCookies.isNotEmpty ? newCookies : null;
|
||||
handler.next(options);
|
||||
}).catchError((dynamic e, StackTrace s) {
|
||||
final err = DioException(
|
||||
requestOptions: options,
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
);
|
||||
handler.reject(err, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||
final path = response.requestOptions.path;
|
||||
if (path.startsWith(HttpString.appBaseUrl) ||
|
||||
path.startsWith(GStorage.blockServer)) {
|
||||
return handler.next(response);
|
||||
} else {
|
||||
_saveCookies(response).then((_) => handler.next(response)).catchError(
|
||||
(dynamic e, StackTrace s) {
|
||||
final error = DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
);
|
||||
handler.reject(error, true);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
String url = err.requestOptions.uri.toString();
|
||||
debugPrint('🌹🌹ApiInterceptor: $url');
|
||||
if (url.contains('heartbeat') ||
|
||||
url.contains('seg.so') ||
|
||||
url.contains('online/total') ||
|
||||
url.contains('github') ||
|
||||
(url.contains('skipSegments') && err.requestOptions.method == 'GET')) {
|
||||
// skip
|
||||
} else {
|
||||
dioError(err).then((res) => SmartDialog.showToast(res + url));
|
||||
}
|
||||
if (err.response != null &&
|
||||
!err.response!.requestOptions.path.startsWith(HttpString.appBaseUrl)) {
|
||||
_saveCookies(err.response!).then((_) => handler.next(err)).catchError(
|
||||
(dynamic e, StackTrace s) {
|
||||
final error = DioException(
|
||||
requestOptions: err.response!.requestOptions,
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
);
|
||||
handler.next(error);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
handler.next(err);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveCookies(Response response) async {
|
||||
final account = (response.requestOptions.extra['account'] as Account? ??
|
||||
_findAccount(response.requestOptions.path));
|
||||
final setCookies = response.headers[HttpHeaders.setCookieHeader];
|
||||
if (setCookies == null || setCookies.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final List<Cookie> cookies = setCookies
|
||||
.map((str) => str.split(_setCookieReg))
|
||||
.expand((cookie) => cookie)
|
||||
.where((cookie) => cookie.isNotEmpty)
|
||||
.map((str) => Cookie.fromSetCookieValue(str))
|
||||
.toList();
|
||||
final statusCode = response.statusCode ?? 0;
|
||||
final locations = response.headers[HttpHeaders.locationHeader] ?? [];
|
||||
final isRedirectRequest = statusCode >= 300 && statusCode < 400;
|
||||
final originalUri = response.requestOptions.uri;
|
||||
final realUri = originalUri.resolveUri(response.realUri);
|
||||
await account.cookieJar.saveFromResponse(realUri, cookies);
|
||||
if (isRedirectRequest && locations.isNotEmpty) {
|
||||
final originalUri = response.realUri;
|
||||
await Future.wait(
|
||||
locations.map(
|
||||
(location) => account.cookieJar.saveFromResponse(
|
||||
// Resolves the location based on the current Uri.
|
||||
originalUri.resolve(location),
|
||||
cookies,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
await account.onChange();
|
||||
}
|
||||
|
||||
Account _findAccount(String path) => loginApi.contains(path)
|
||||
? AnonymousAccount()
|
||||
: Accounts.get(AccountType.values.firstWhere(
|
||||
(i) => apiTypeSet[i]?.contains(path) == true,
|
||||
orElse: () => AccountType.main));
|
||||
|
||||
static Future<String> dioError(DioException error) async {
|
||||
switch (error.type) {
|
||||
case DioExceptionType.badCertificate:
|
||||
return '证书有误!';
|
||||
case DioExceptionType.badResponse:
|
||||
return '服务器异常,请稍后重试!';
|
||||
case DioExceptionType.cancel:
|
||||
return '请求已被取消,请重新请求';
|
||||
case DioExceptionType.connectionError:
|
||||
return '连接错误,请检查网络设置';
|
||||
case DioExceptionType.connectionTimeout:
|
||||
return '网络连接超时,请检查网络设置';
|
||||
case DioExceptionType.receiveTimeout:
|
||||
return '响应超时,请稍后重试!';
|
||||
case DioExceptionType.sendTimeout:
|
||||
return '发送请求超时,请检查网络设置';
|
||||
case DioExceptionType.unknown:
|
||||
final String res =
|
||||
(await Connectivity().checkConnectivity()).first.title;
|
||||
return '$res网络异常 ${error.error}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension _ConnectivityResultExt on ConnectivityResult {
|
||||
String get title => const ['蓝牙', 'Wi-Fi', '局域', '流量', '无', '代理', '其他'][index];
|
||||
}
|
||||
28
lib/utils/accounts/account_type_adapter.dart
Normal file
28
lib/utils/accounts/account_type_adapter.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
import '../storage.dart' show AccountType;
|
||||
|
||||
class AccountTypeAdapter extends TypeAdapter<AccountType> {
|
||||
@override
|
||||
final int typeId = 10;
|
||||
|
||||
@override
|
||||
AccountType read(BinaryReader reader) =>
|
||||
AccountType.values.getOrNull(reader.readByte()) ?? AccountType.main;
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, AccountType obj) {
|
||||
writer.writeByte(obj.index);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is AccountTypeAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
28
lib/utils/accounts/cookie_jar_adapter.dart
Normal file
28
lib/utils/accounts/cookie_jar_adapter.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
import 'package:cookie_jar/cookie_jar.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
import 'account.dart';
|
||||
|
||||
class BiliCookieJarAdapter extends TypeAdapter<DefaultCookieJar> {
|
||||
@override
|
||||
final int typeId = 8;
|
||||
|
||||
@override
|
||||
DefaultCookieJar read(BinaryReader reader) =>
|
||||
BiliCookieJar.fromJson(reader.readMap().cast<String, String>());
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, DefaultCookieJar obj) {
|
||||
writer.writeMap(obj.toJson());
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is BiliCookieJarAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ class Data {
|
||||
}
|
||||
|
||||
static Future historyStatus() async {
|
||||
if (GStorage.userInfo.get('userInfoCache') == null) {
|
||||
if (!Accounts.main.isLogin) {
|
||||
return;
|
||||
}
|
||||
var res = await UserHttp.historyStatus();
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:PiliPlus/http/constants.dart';
|
||||
import 'package:PiliPlus/http/init.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/models/common/dynamics_type.dart';
|
||||
import 'package:PiliPlus/models/common/tab_type.dart' hide tabsConfig;
|
||||
@@ -12,6 +9,7 @@ import 'package:PiliPlus/pages/bangumi/controller.dart';
|
||||
import 'package:PiliPlus/pages/dynamics/tab/controller.dart';
|
||||
import 'package:PiliPlus/pages/live/controller.dart';
|
||||
import 'package:PiliPlus/pages/main/controller.dart';
|
||||
import 'package:PiliPlus/utils/accounts/account.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -30,52 +28,31 @@ import 'package:PiliPlus/http/user.dart';
|
||||
class LoginUtils {
|
||||
static final random = Random();
|
||||
|
||||
static Future onLogin(Map<String, dynamic> tokenInfo, jsonCookieInfo) async {
|
||||
static Future onLoginMain() async {
|
||||
final account = Accounts.main;
|
||||
try {
|
||||
GStorage.localCache.put(LocalCacheKey.accessKey, {
|
||||
'mid': tokenInfo['mid'],
|
||||
'value': tokenInfo['access_token'] ?? tokenInfo['value'],
|
||||
'refresh': tokenInfo['refresh_token'] ?? tokenInfo['refresh']
|
||||
});
|
||||
List<dynamic> cookieInfo = jsonCookieInfo['cookies'];
|
||||
List<Cookie> cookies = [];
|
||||
String cookieStrings = cookieInfo.map((cookie) {
|
||||
String cstr =
|
||||
'${cookie['name']}=${cookie['value']};Domain=.bilibili.com;Path=/;';
|
||||
cookies.add(Cookie.fromSetCookieValue(cstr));
|
||||
return cstr;
|
||||
}).join('');
|
||||
List<String> urls = [
|
||||
HttpString.baseUrl,
|
||||
HttpString.apiBaseUrl,
|
||||
HttpString.tUrl
|
||||
];
|
||||
for (var url in urls) {
|
||||
await Request.cookieManager.cookieJar
|
||||
.saveFromResponse(Uri.parse(url), cookies);
|
||||
}
|
||||
Request.dio.options.headers['cookie'] = cookieStrings;
|
||||
await WebviewCookieManager().setCookies(cookies);
|
||||
for (Cookie item in cookies) {
|
||||
await web.CookieManager().setCookie(
|
||||
url: web.WebUri(item.domain ?? ''),
|
||||
name: item.name,
|
||||
value: item.value,
|
||||
path: item.path ?? '',
|
||||
domain: item.domain,
|
||||
isSecure: item.secure,
|
||||
isHttpOnly: item.httpOnly,
|
||||
);
|
||||
}
|
||||
final cookies = account.cookieJar.toList();
|
||||
final webManager = web.CookieManager();
|
||||
Future.wait([
|
||||
WebviewCookieManager().setCookies(cookies),
|
||||
...cookies.map((item) => webManager.setCookie(
|
||||
url: web.WebUri(item.domain ?? ''),
|
||||
name: item.name,
|
||||
value: item.value,
|
||||
path: item.path ?? '',
|
||||
domain: item.domain,
|
||||
isSecure: item.secure,
|
||||
isHttpOnly: item.httpOnly,
|
||||
))
|
||||
]);
|
||||
} catch (e) {
|
||||
SmartDialog.showToast('设置登录态失败,$e');
|
||||
}
|
||||
final result = await UserHttp.userInfo();
|
||||
if (result['status'] && result['data'].isLogin) {
|
||||
SmartDialog.showToast('登录成功,当前采用「'
|
||||
'${GStorage.setting.get(SettingBoxKey.defaultRcmdType, defaultValue: 'app')}'
|
||||
'端」推荐');
|
||||
await GStorage.userInfo.put('userInfoCache', result['data']);
|
||||
final UserInfoData data = result['data'];
|
||||
if (result['status'] && data.isLogin!) {
|
||||
SmartDialog.showToast('main登录成功');
|
||||
await GStorage.userInfo.put('userInfoCache', data);
|
||||
try {
|
||||
Get.find<MineController>()
|
||||
..isLogin.value = true
|
||||
@@ -85,14 +62,14 @@ class LoginUtils {
|
||||
try {
|
||||
Get.find<HomeController>()
|
||||
..isLogin.value = true
|
||||
..userFace.value = result['data'].face;
|
||||
..userFace.value = data.face!;
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
Get.find<DynamicsController>()
|
||||
..isLogin.value = true
|
||||
..ownerMid = result['data'].mid
|
||||
..face = result['data'].face
|
||||
..ownerMid = data.mid
|
||||
..face = data.face
|
||||
..onRefresh();
|
||||
} catch (_) {}
|
||||
|
||||
@@ -105,7 +82,7 @@ class LoginUtils {
|
||||
|
||||
try {
|
||||
Get.find<MediaController>()
|
||||
..mid = result['data'].mid
|
||||
..mid = data.mid
|
||||
..onRefresh();
|
||||
} catch (_) {}
|
||||
|
||||
@@ -128,19 +105,18 @@ class LoginUtils {
|
||||
} catch (_) {}
|
||||
} else {
|
||||
// 获取用户信息失败
|
||||
await Accounts.set(AccountType.main, await account.logout());
|
||||
SmartDialog.showNotify(
|
||||
msg: '登录失败,请检查cookie是否正确,${result['message']}',
|
||||
notifyType: NotifyType.warning);
|
||||
}
|
||||
}
|
||||
|
||||
static Future onLogout() async {
|
||||
await Request.cookieManager.cookieJar.deleteAll();
|
||||
await web.CookieManager().deleteAllCookies();
|
||||
Request.dio.options.headers['cookie'] = '';
|
||||
|
||||
await GStorage.userInfo.delete('userInfoCache');
|
||||
await GStorage.localCache.delete(LocalCacheKey.accessKey);
|
||||
static Future onLogoutMain() async {
|
||||
await Future.wait([
|
||||
web.CookieManager().deleteAllCookies(),
|
||||
GStorage.userInfo.delete('userInfoCache'),
|
||||
]);
|
||||
|
||||
try {
|
||||
Get.find<MainController>().isLogin.value = false;
|
||||
@@ -151,7 +127,7 @@ class LoginUtils {
|
||||
..userInfo.value = UserInfoData()
|
||||
..userStat.value = UserStat()
|
||||
..isLogin.value = false;
|
||||
MineController.anonymity.value = false;
|
||||
// MineController.anonymity.value = false;
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:PiliPlus/common/widgets/pair.dart';
|
||||
import 'package:PiliPlus/common/widgets/refresh_indicator.dart'
|
||||
show kDragContainerExtentPercentage, displacement;
|
||||
import 'package:PiliPlus/http/constants.dart';
|
||||
import 'package:PiliPlus/http/index.dart';
|
||||
import 'package:PiliPlus/models/common/dynamic_badge_mode.dart';
|
||||
import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart';
|
||||
import 'package:PiliPlus/models/common/sponsor_block/skip_type.dart';
|
||||
@@ -15,8 +16,15 @@ import 'package:PiliPlus/models/video/play/CDN.dart';
|
||||
import 'package:PiliPlus/models/video/play/quality.dart';
|
||||
import 'package:PiliPlus/models/video/play/subtitle.dart';
|
||||
import 'package:PiliPlus/pages/member/new/controller.dart' show MemberTabType;
|
||||
import 'package:PiliPlus/pages/mine/index.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/models/bottom_progress_behavior.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/models/fullscreen_mode.dart';
|
||||
import 'package:PiliPlus/utils/accounts/account.dart';
|
||||
import 'package:PiliPlus/utils/accounts/account_adapter.dart';
|
||||
import 'package:PiliPlus/utils/accounts/cookie_jar_adapter.dart';
|
||||
import 'package:PiliPlus/utils/accounts/account_type_adapter.dart';
|
||||
import 'package:PiliPlus/utils/login.dart';
|
||||
import 'package:cookie_jar/cookie_jar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
@@ -33,9 +41,9 @@ class GStorage {
|
||||
static late final Box<dynamic> setting;
|
||||
static late final Box<dynamic> video;
|
||||
|
||||
static bool get isLogin => userInfo.get('userInfoCache') != null;
|
||||
// static bool get isLogin => userInfo.get('userInfoCache') != null;
|
||||
|
||||
static get ownerMid => userInfo.get('userInfoCache')?.mid;
|
||||
// static get ownerMid => userInfo.get('userInfoCache')?.mid;
|
||||
|
||||
static List<double> get speedList => List<double>.from(
|
||||
video.get(
|
||||
@@ -192,8 +200,8 @@ class GStorage {
|
||||
static int get minLikeRatioForRecommend =>
|
||||
setting.get(SettingBoxKey.minLikeRatioForRecommend, defaultValue: 0);
|
||||
|
||||
static String get defaultRcmdType =>
|
||||
setting.get(SettingBoxKey.defaultRcmdType, defaultValue: 'app');
|
||||
static bool get appRcmd =>
|
||||
setting.get(SettingBoxKey.appRcmd, defaultValue: true);
|
||||
|
||||
static String get defaultSystemProxyHost =>
|
||||
setting.get(SettingBoxKey.systemProxyHost, defaultValue: '');
|
||||
@@ -493,6 +501,9 @@ class GStorage {
|
||||
video = await Hive.openBox('video');
|
||||
displacement = GStorage.refreshDisplacement;
|
||||
kDragContainerExtentPercentage = GStorage.refreshDragPercentage;
|
||||
|
||||
await Accounts.init();
|
||||
|
||||
// 设置全局变量
|
||||
GlobalData()
|
||||
..imgQuality = defaultPicQa
|
||||
@@ -521,6 +532,9 @@ class GStorage {
|
||||
Hive.registerAdapter(LevelInfoAdapter());
|
||||
Hive.registerAdapter(HotSearchModelAdapter());
|
||||
Hive.registerAdapter(HotSearchItemAdapter());
|
||||
Hive.registerAdapter(BiliCookieJarAdapter());
|
||||
Hive.registerAdapter(LoginAccountAdapter());
|
||||
Hive.registerAdapter(AccountTypeAdapter());
|
||||
}
|
||||
|
||||
static Future<void> close() async {
|
||||
@@ -536,6 +550,7 @@ class GStorage {
|
||||
setting.close();
|
||||
video.compact();
|
||||
video.close();
|
||||
Accounts.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -587,11 +602,11 @@ class SettingBoxKey {
|
||||
continuePlayInBackground = 'continuePlayInBackground',
|
||||
|
||||
/// 隐私
|
||||
anonymity = 'anonymity',
|
||||
// anonymity = 'anonymity',
|
||||
|
||||
/// 推荐
|
||||
enableRcmdDynamic = 'enableRcmdDynamic',
|
||||
defaultRcmdType = 'defaultRcmdType',
|
||||
appRcmd = 'appRcmd',
|
||||
enableSaveLastData = 'enableSaveLastData',
|
||||
minDurationForRcmd = 'minDurationForRcmd',
|
||||
minLikeRatioForRecommend = 'minLikeRatioForRecommend',
|
||||
@@ -742,8 +757,8 @@ class LocalCacheKey {
|
||||
blackMidsList = 'blackMidsList',
|
||||
// 弹幕屏蔽规则
|
||||
danmakuFilterRule = 'danmakuFilterRule',
|
||||
// access_key
|
||||
accessKey = 'accessKey',
|
||||
// // access_key
|
||||
// accessKey = 'accessKey',
|
||||
|
||||
//
|
||||
mixinKey = 'mixinKey',
|
||||
@@ -768,3 +783,112 @@ class VideoBoxKey {
|
||||
// 画面填充比例
|
||||
cacheVideoFit = 'cacheVideoFit';
|
||||
}
|
||||
|
||||
class Accounts {
|
||||
static late final Box<LoginAccount> account;
|
||||
static final Map<AccountType, Account> accountMode = {};
|
||||
static Account get main => accountMode[AccountType.main]!;
|
||||
// static set main(Account account) => set(AccountType.main, account);
|
||||
|
||||
static Future<void> init() async {
|
||||
account = await Hive.openBox('account',
|
||||
compactionStrategy: (int entries, int deletedEntries) {
|
||||
return deletedEntries > 2;
|
||||
});
|
||||
await _migrate();
|
||||
}
|
||||
|
||||
static Future<void> _migrate() async {
|
||||
final Directory tempDir = await getApplicationSupportDirectory();
|
||||
final String tempPath = "${tempDir.path}/.plpl/";
|
||||
final Directory dir = Directory(tempPath);
|
||||
if (await dir.exists()) {
|
||||
debugPrint('migrating...');
|
||||
final cookieJar =
|
||||
PersistCookieJar(ignoreExpires: true, storage: FileStorage(tempPath));
|
||||
await cookieJar.forceInit();
|
||||
final cookies = DefaultCookieJar(ignoreExpires: true)
|
||||
..domainCookies.addAll(cookieJar.domainCookies);
|
||||
final localAccessKey =
|
||||
GStorage.localCache.get('accessKey', defaultValue: {});
|
||||
|
||||
final isLogin =
|
||||
cookies.domainCookies['bilibili.com']?['/']?['SESSDATA'] != null;
|
||||
|
||||
await Future.wait([
|
||||
GStorage.localCache.delete('accessKey'),
|
||||
dir.delete(recursive: true),
|
||||
if (isLogin)
|
||||
LoginAccount(cookies, localAccessKey['value'],
|
||||
localAccessKey['refresh'], AccountType.values.toSet())
|
||||
.onChange()
|
||||
]);
|
||||
debugPrint('migrated successfully');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> refresh() async {
|
||||
for (var a in account.values) {
|
||||
for (var t in a.type) {
|
||||
accountMode[t] = a;
|
||||
}
|
||||
}
|
||||
for (var type in AccountType.values) {
|
||||
accountMode[type] ??= AnonymousAccount();
|
||||
}
|
||||
await Future.wait((accountMode.values.toSet()
|
||||
..retainWhere((i) => !i.activited))
|
||||
.map((i) => Request.buvidActive(i)));
|
||||
}
|
||||
|
||||
static Future<void> clear() async {
|
||||
await account.clear();
|
||||
for (var i in AccountType.values) {
|
||||
accountMode[i] = AnonymousAccount();
|
||||
}
|
||||
if (!AnonymousAccount().activited) {
|
||||
Request.buvidActive(AnonymousAccount());
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> close() async {
|
||||
account.compact();
|
||||
account.close();
|
||||
}
|
||||
|
||||
static Future<void> set(AccountType key, Account account) async {
|
||||
await (accountMode[key]?..type.remove(key))?.onChange();
|
||||
accountMode[key] = account..type.add(key);
|
||||
await account.onChange();
|
||||
if (!account.activited) await Request.buvidActive(account);
|
||||
switch (key) {
|
||||
case AccountType.main:
|
||||
if (account.isLogin) {
|
||||
await LoginUtils.onLoginMain();
|
||||
} else {
|
||||
await LoginUtils.onLogoutMain();
|
||||
}
|
||||
break;
|
||||
case AccountType.heartbeat:
|
||||
MineController.anonymity.value = !account.isLogin;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static Account get(AccountType key) {
|
||||
return accountMode[key]!;
|
||||
}
|
||||
}
|
||||
|
||||
enum AccountType {
|
||||
main,
|
||||
heartbeat,
|
||||
recommend,
|
||||
video,
|
||||
}
|
||||
|
||||
extension AccountTypeExt on AccountType {
|
||||
String get title => const ['主账号', '记录观看', '推荐', '视频取流'][index];
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:PiliPlus/build_config.dart';
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart';
|
||||
import 'package:PiliPlus/common/widgets/radio_widget.dart';
|
||||
import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart';
|
||||
@@ -24,6 +25,7 @@ import 'package:PiliPlus/pages/dynamics/tab/controller.dart';
|
||||
import 'package:PiliPlus/pages/later/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/detail/introduction/widgets/fav_panel.dart';
|
||||
import 'package:PiliPlus/pages/video/detail/introduction/widgets/group_panel.dart';
|
||||
import 'package:PiliPlus/utils/accounts/account.dart';
|
||||
import 'package:PiliPlus/utils/app_scheme.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/feed_back.dart';
|
||||
@@ -40,7 +42,6 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_navigation/src/dialog/dialog_route.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:html/dom.dart' as dom;
|
||||
@@ -778,7 +779,7 @@ class Utils {
|
||||
dynamic response = await Request().get(
|
||||
'${HttpString.spaceBaseUrl}/$mid/dynamic',
|
||||
options: Options(
|
||||
extra: {'clearCookie': true},
|
||||
extra: {'account': AnonymousAccount()},
|
||||
),
|
||||
);
|
||||
dom.Document document = html_parser.parse(response.data);
|
||||
@@ -1137,16 +1138,16 @@ class Utils {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<String> getCookiePath() async {
|
||||
final Directory tempDir = await getApplicationSupportDirectory();
|
||||
final String tempPath = "${tempDir.path}/.plpl/";
|
||||
final Directory dir = Directory(tempPath);
|
||||
final bool b = await dir.exists();
|
||||
if (!b) {
|
||||
dir.createSync(recursive: true);
|
||||
}
|
||||
return tempPath;
|
||||
}
|
||||
// static Future<String> getCookiePath() async {
|
||||
// final Directory tempDir = await getApplicationSupportDirectory();
|
||||
// final String tempPath = "${tempDir.path}/.plpl/";
|
||||
// final Directory dir = Directory(tempPath);
|
||||
// final bool b = await dir.exists();
|
||||
// if (!b) {
|
||||
// dir.createSync(recursive: true);
|
||||
// }
|
||||
// return tempPath;
|
||||
// }
|
||||
|
||||
static String numFormat(dynamic number) {
|
||||
if (number == null) {
|
||||
@@ -1596,18 +1597,17 @@ class Utils {
|
||||
return height;
|
||||
}
|
||||
|
||||
static String appSign(
|
||||
Map<String, String> params, String appkey, String appsec) {
|
||||
static void appSign(Map<String, dynamic> params,
|
||||
[String appkey = Constants.appKey, String appsec = Constants.appSec]) {
|
||||
params['appkey'] = appkey;
|
||||
var searchParams = Uri(queryParameters: params).query;
|
||||
var sortedParams = searchParams.split('&')..sort();
|
||||
var sortedQueryString = sortedParams.join('&');
|
||||
var searchParams = Uri(
|
||||
queryParameters:
|
||||
params.map((key, value) => MapEntry(key, value.toString()))).query;
|
||||
var sortedQueryString = (searchParams.split('&')..sort()).join('&');
|
||||
|
||||
var appsecString = sortedQueryString + appsec;
|
||||
var md5Digest = md5.convert(utf8.encode(appsecString));
|
||||
var md5String = md5Digest.toString(); // 获取MD5哈希值
|
||||
|
||||
return md5String;
|
||||
params['sign'] = md5
|
||||
.convert(utf8.encode(sortedQueryString + appsec))
|
||||
.toString(); // 获取MD5哈希值
|
||||
}
|
||||
|
||||
static List<int> generateRandomBytes(int minLength, int maxLength) {
|
||||
|
||||
Reference in New Issue
Block a user