feat: account manager (#468)

* feat: account manager

* remove dep

* some fixes

* migrate accounts

* reimplement clearCookie
This commit is contained in:
My-Responsitories
2025-03-19 13:19:32 +08:00
committed by GitHub
parent 94fa0652ac
commit b15fdfa2ff
47 changed files with 1233 additions and 800 deletions

View File

@@ -131,7 +131,7 @@ class _ListSheetContentState extends CommonSlidePageState<ListSheetContent>
reverse = _isList
? List.generate(widget.season.sections.length, (_) => false)
: [false];
if (GStorage.isLogin && widget.bvid != null && widget.season != null) {
if (Accounts.main.isLogin && widget.bvid != null && widget.season != null) {
_favStream ??= StreamController<int>();
() async {
dynamic result = await VideoHttp.videoRelation(bvid: widget.bvid);

View File

@@ -68,8 +68,7 @@ class VideoCustomActions {
VideoCustomAction(
'不感兴趣', 'dislike', Icon(MdiIcons.thumbDownOutline, size: 16),
() async {
String? accessKey = GStorage.localCache
.get(LocalCacheKey.accessKey, defaultValue: {})['value'];
String? accessKey = Accounts.get(AccountType.recommend).accessKey;
if (accessKey == null || accessKey == "") {
SmartDialog.showToast("请退出账号后重新登录");
return;

View File

@@ -43,16 +43,12 @@ class GrpcRepo {
static const gzipEncoder = GZipEncoder();
static const gzipDecoder = GZipDecoder();
static final bool _isLogin = GStorage.userInfo.get('userInfoCache') != null;
static final int? _mid = GStorage.userInfo.get('userInfoCache')?.mid;
static final String? _accessKey = GStorage.localCache
.get(LocalCacheKey.accessKey, defaultValue: {})['value'];
static final String? _accessKey = Accounts.main.accessKey;
static const _build = 1462100;
static const _biliChannel = 'bili';
static const _mobiApp = 'android_hd';
static const _phone = 'phone';
static final _eId = _isLogin ? Utils.genAuroraEid(_mid!) : '';
static final _buvid = LoginUtils.buvid;
static final _traceId = Utils.genTraceId();
static final _sessionId = Utils.generateRandomString(8);
@@ -63,11 +59,9 @@ class GrpcRepo {
'gzip-accept-encoding': 'gzip,identity',
'user-agent': '${Constants.userAgent} grpc-java-cronet/1.36.1',
'x-bili-gaia-vtoken': '',
'x-bili-aurora-eid': _isLogin ? _eId : '',
'x-bili-mid': _isLogin ? _mid.toString() : '0',
'x-bili-aurora-zone': '',
'x-bili-trace-id': _traceId,
if (_isLogin) 'authorization': 'identify_v1 $_accessKey',
if (_accessKey != null) 'authorization': 'identify_v1 $_accessKey',
'buvid': _buvid,
'bili-http-engine': 'cronet',
'te': 'trailers',

View File

@@ -622,6 +622,8 @@ class Api {
static const qrcodePoll =
'${HttpString.passBaseUrl}/x/passport-tv-login/qrcode/poll';
static const logout = '${HttpString.passBaseUrl}/login/exit/v2';
/// 置顶视频
static const getTopVideoApi = '/x/space/top/arc';

View File

@@ -1,4 +1,5 @@
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/utils/accounts/account.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:dio/dio.dart';
@@ -92,7 +93,7 @@ class DynamicsHttp {
dynamic id,
dynamic rid,
dynamic type,
bool? clearCookie,
bool clearCookie = false,
}) async {
var res = await Request().get(
Api.dynamicDetail,
@@ -104,7 +105,7 @@ class DynamicsHttp {
'features': 'itemOpusStyle',
},
options:
clearCookie == true ? Options(extra: {'clearCookie': true}) : null,
clearCookie ? Options(extra: {'account': AnonymousAccount()}) : null,
);
if (res.data['code'] == 0) {
try {

View File

@@ -4,20 +4,17 @@ import 'dart:developer';
import 'dart:io';
import 'dart:math' show Random;
import 'package:PiliPlus/build_config.dart';
import 'package:PiliPlus/utils/accounts/account.dart';
import 'package:PiliPlus/utils/accounts/account_manager/account_mgr.dart';
import 'package:archive/archive.dart';
import 'package:brotli/brotli.dart';
import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:dio_http2_adapter/dio_http2_adapter.dart';
import 'package:flutter/material.dart';
import 'package:PiliPlus/utils/id_utils.dart';
import '../utils/storage.dart';
import '../utils/utils.dart';
import 'api.dart';
import 'constants.dart';
import 'interceptor.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart' as web;
class Request {
@@ -25,101 +22,69 @@ class Request {
static const brotilDecoder = BrotliDecoder();
static final Request _instance = Request._internal();
static late CookieManager cookieManager;
static late AccountManager accountManager;
static late final Dio dio;
factory Request() => _instance;
late bool enableSystemProxy;
late String systemProxyHost;
late String systemProxyPort;
static final _rand = Random();
static final RegExp spmPrefixExp =
RegExp(r'<meta name="spm_prefix" content="([^"]+?)">');
/// 设置cookie
static setCookie() async {
final String cookiePath = await Utils.getCookiePath();
final PersistCookieJar cookieJar = PersistCookieJar(
ignoreExpires: true,
storage: FileStorage(cookiePath),
);
cookieManager = CookieManager(cookieJar);
dio.interceptors.add(cookieManager);
dio.interceptors.add(ApiInterceptor());
final List<Cookie> cookies = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.baseUrl));
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 userInfo = GStorage.userInfo.get('userInfoCache');
if (userInfo?.mid != null) {
final List<Cookie> tUrlCookies = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.tUrl));
if (tUrlCookies.isEmpty) {
try {
await dio.head(HttpString.tUrl);
} catch (e) {
log("setCookie, ${e.toString()}");
}
}
setOptionsHeaders(userInfo);
}
try {
await buvidActivate();
} catch (e) {
log("setCookie, ${e.toString()}");
}
// final String cookieString = cookies
// .map((Cookie cookie) => '${cookie.name}=${cookie.value}')
// .join('; ');
// dio.options.headers['cookie'] = cookieString;
accountManager = AccountManager();
dio.interceptors.add(accountManager);
await Accounts.refresh();
final List<Cookie> cookies = Accounts.main.cookieJar.toList();
final webManager = web.CookieManager();
await Future.wait(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,
)));
}
// 从cookie中获取 csrf token
static Future<String> getCsrf() async {
List<Cookie> cookies = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.apiBaseUrl));
return cookies
.firstWhere((e) => e.name == 'bili_jct', orElse: () => Cookie('', ''))
.value;
return Accounts.main.csrf;
}
static setOptionsHeaders(userInfo) {
dio.options.headers['x-bili-mid'] = userInfo.mid.toString();
dio.options.headers['x-bili-aurora-eid'] =
IdUtils.genAuroraEid(userInfo.mid);
}
static Future<void> buvidActive(Account account) async {
// 这样线程不安全, 但仍按预期进行
if (account.activited) return;
account.activited = true;
try {
final html = await Request().get(Api.dynamicSpmPrefix,
options: Options(extra: {'account': account}));
final String spmPrefix = spmPrefixExp.firstMatch(html.data)!.group(1)!;
final String randPngEnd = base64.encode(
List<int>.generate(32, (_) => _rand.nextInt(256)) +
List<int>.filled(4, 0) +
[73, 69, 78, 68] +
List<int>.generate(4, (_) => _rand.nextInt(256)));
static Future buvidActivate() async {
var html = await Request().get(Api.dynamicSpmPrefix);
String spmPrefix = spmPrefixExp.firstMatch(html.data)!.group(1)!;
Random rand = Random();
String randPngEnd = base64.encode(
List<int>.generate(32, (_) => rand.nextInt(256)) +
List<int>.filled(4, 0) +
[73, 69, 78, 68] +
List<int>.generate(4, (_) => rand.nextInt(256)));
String jsonData = json.encode({
'3064': 1,
'39c8': '$spmPrefix.fp.risk',
'3c43': {
'adca': 'Linux',
'bfe9': randPngEnd.substring(randPngEnd.length - 50),
},
});
String jsonData = json.encode({
'3064': 1,
'39c8': '$spmPrefix.fp.risk',
'3c43': {
'adca': 'Linux',
'bfe9': randPngEnd.substring(randPngEnd.length - 50),
},
});
await Request().post(Api.activateBuvidApi,
data: {'payload': jsonData},
options: Options(contentType: Headers.jsonContentType));
await Request().post(Api.activateBuvidApi,
data: {'payload': jsonData},
options: Options(contentType: Headers.jsonContentType));
;
} catch (e) {
log("setCookie, $e");
}
}
/*
@@ -225,7 +190,7 @@ class Request {
} on DioException catch (e) {
Response errResponse = Response(
data: {
'message': await ApiInterceptor.dioError(e)
'message': await AccountManager.dioError(e)
}, // 将自定义 Map 数据赋值给 Response 的 data 属性
statusCode: -1,
requestOptions: RequestOptions(),
@@ -254,7 +219,7 @@ class Request {
} on DioException catch (e) {
Response errResponse = Response(
data: {
'message': await ApiInterceptor.dioError(e)
'message': await AccountManager.dioError(e)
}, // 将自定义 Map 数据赋值给 Response 的 data 属性
statusCode: -1,
requestOptions: RequestOptions(),
@@ -279,7 +244,7 @@ class Request {
return response.data;
} on DioException catch (e) {
debugPrint('downloadFile error: $e');
return Future.error(ApiInterceptor.dioError(e));
return Future.error(AccountManager.dioError(e));
}
}

View File

@@ -1,143 +0,0 @@
import 'package:PiliPlus/http/api.dart';
import 'package:PiliPlus/pages/mine/controller.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';
class ApiInterceptor extends Interceptor {
static const List<String> anonymityList = [
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,
];
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
void onRemoveCookie() {
options.headers.remove('x-bili-mid');
options.headers.remove('x-bili-aurora-eid');
options.headers.remove('x-bili-aurora-zone');
options.headers['cookie'] = '';
options.queryParameters.remove('access_key');
options.queryParameters.remove('csrf');
options.queryParameters.remove('csrf_token');
if (options.data is Map) {
options.data.remove('access_key');
options.data.remove('csrf');
options.data.remove('csrf_token');
}
}
// app端不需要cookie
// if (options.uri.host == 'app.bilibili.com') {
// options.headers.remove('cookie');
// }
if (options.extra['clearCookie'] == true) {
onRemoveCookie();
} else if (MineController.anonymity.value) {
String uri = options.uri.toString();
for (var i in anonymityList) {
// 如果请求的url包含无痕列表中的url则清空cookie
// 但需要保证匹配到url的后半部分不再出现/符号,否则会误伤
int index = uri.indexOf(i);
if (index == -1) continue;
if (uri.lastIndexOf('/') >= index + i.length) continue;
//SmartDialog.showToast('触发无痕模式\n\n$i\n\n${options.uri}');
onRemoveCookie();
break;
}
}
handler.next(options);
}
// @override
// void onResponse(Response response, ResponseInterceptorHandler handler) {
// try {
// if (response.statusCode == 302) {
// final List<String> locations = response.headers['location']!;
// if (locations.isNotEmpty) {
// if (locations.first.startsWith('https://www.mcbbs.net')) {
// debugPrint('ApiInterceptor@@@@@: ${locations.first}');
// final Uri uri = Uri.parse(locations.first);
// final String? accessKey = uri.queryParameters['access_key'];
// final String? mid = uri.queryParameters['mid'];
// try {
// GStorage.localCache.put(LocalCacheKey.accessKey,
// <String, String?>{'mid': mid, 'value': accessKey});
// } catch (_) {}
// }
// }
// }
// } catch (err) {
// debugPrint('ApiInterceptor: $err');
// }
// handler.next(response);
// }
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
// 处理网络请求错误
// handler.next(err);
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 {
SmartDialog.showToast(
await dioError(err) + url,
// displayType: SmartToastType.onlyRefresh,
// displayTime: const Duration(milliseconds: 1200),
);
}
super.onError(err, handler);
}
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];
}

View File

@@ -1,4 +1,5 @@
import 'dart:convert';
import 'package:PiliPlus/utils/accounts/account.dart';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
import 'package:encrypt/encrypt.dart';
@@ -11,9 +12,7 @@ import 'index.dart';
class LoginHttp {
static final String deviceId = LoginUtils.genDeviceId();
static final String buvid = LoginUtils.buvid;
static const String host = 'passport.bilibili.com';
static final Map<String, String> headers = {
'Host': host,
'buvid': buvid,
'env': 'prod',
'app-key': 'android_hd',
@@ -27,21 +26,14 @@ class LoginHttp {
static Future<Map<String, dynamic>> getHDcode() async {
var params = {
'appkey': Constants.appKey,
// 'local_id': 'Y952A395BB157D305D8A8340FC2AAECECE17',
'local_id': '0',
//精确到秒的时间戳
'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(),
'platform': 'android',
'mobi_app': 'android_hd',
};
String sign = Utils.appSign(
params,
Constants.appKey,
Constants.appSec,
);
var res = await Request()
.post(Api.getTVCode, queryParameters: {...params, 'sign': sign});
Utils.appSign(params);
var res = await Request().post(Api.getTVCode, queryParameters: params);
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
@@ -52,18 +44,12 @@ class LoginHttp {
static Future codePoll(String authCode) async {
var params = {
'appkey': Constants.appKey,
'auth_code': authCode,
'local_id': '0',
'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(),
};
String sign = Utils.appSign(
params,
Constants.appKey,
Constants.appSec,
);
var res = await Request()
.post(Api.qrcodePoll, queryParameters: {...params, 'sign': sign});
Utils.appSign(params);
var res = await Request().post(Api.qrcodePoll, queryParameters: params);
return {
'status': res.data['code'] == 0,
'code': res.data['code'],
@@ -106,7 +92,6 @@ class LoginHttp {
}) async {
int timestamp = DateTime.now().millisecondsSinceEpoch;
var data = {
'appkey': Constants.appKey,
'build': '2001100',
'buvid': buvid,
'c_locale': 'zh_CN',
@@ -129,15 +114,11 @@ class LoginHttp {
'tel': tel,
'ts': (timestamp ~/ 1000).toString(),
};
String sign = Utils.appSign(
data,
Constants.appKey,
Constants.appSec,
);
Utils.appSign(data);
var res = await Request().post(
Api.appSmsCode,
data: {...data, 'sign': sign},
data: data,
options: Options(
contentType: Headers.formUrlEncodedContentType,
headers: headers,
@@ -211,7 +192,6 @@ class LoginHttp {
Encrypter(RSA(publicKey: publicKey)).encrypt(salt + password).base64;
Map<String, String> data = {
'appkey': Constants.appKey,
'bili_local_id': deviceId,
'build': '2001100',
'buvid': buvid,
@@ -242,15 +222,7 @@ class LoginHttp {
'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(),
'username': username,
};
String sign = Utils.appSign(
data,
Constants.appKey,
Constants.appSec,
);
data['sign'] = sign;
data.map((key, value) {
return MapEntry<String, dynamic>(key, value);
});
Utils.appSign(data);
var res = await Request().post(
Api.loginByPwdApi,
data: data,
@@ -287,7 +259,6 @@ class LoginHttp {
}) async {
dynamic publicKey = RSAKeyParser().parse(key);
Map<String, String> data = {
'appkey': Constants.appKey,
'bili_local_id': deviceId,
'build': '2001100',
'buvid': buvid,
@@ -316,15 +287,7 @@ class LoginHttp {
'tel': tel,
'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(),
};
String sign = Utils.appSign(
data,
Constants.appKey,
Constants.appSec,
);
data['sign'] = sign;
data.map((key, value) {
return MapEntry<String, dynamic>(key, value);
});
Utils.appSign(data);
var res = await Request().post(
Api.logInByAppSms,
data: data,
@@ -404,12 +367,7 @@ class LoginHttp {
if (geeValidate != null) 'gee_validate': geeValidate,
if (recaptchaToken != null) 'recaptcha_token': recaptchaToken,
};
String sign = Utils.appSign(
data,
Constants.appKey,
Constants.appSec,
);
data['sign'] = sign;
Utils.appSign(data);
var res = await Request().post(
Api.safeCenterSmsCode,
data: data,
@@ -449,12 +407,7 @@ class LoginHttp {
'source': source,
'captcha_key': captchaKey,
};
String sign = Utils.appSign(
data,
Constants.appKey,
Constants.appSec,
);
data['sign'] = sign;
Utils.appSign(data);
var res = await Request().post(
Api.safeCenterSmsVerify,
data: data,
@@ -500,15 +453,7 @@ class LoginHttp {
// 'statistics': Constants.statistics,
'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(),
};
String sign = Utils.appSign(
data,
Constants.appKey,
Constants.appSec,
);
data['sign'] = sign;
data.map((key, value) {
return MapEntry<String, dynamic>(key, value);
});
Utils.appSign(data);
var res = await Request().post(
Api.oauth2AccessToken,
data: data,
@@ -529,4 +474,15 @@ class LoginHttp {
};
}
}
static Future<Map> logout(Account account) async {
dynamic res = await Request().post(
Api.logout,
data: {'biliCSRF': account.csrf},
options: Options(
contentType: Headers.formUrlEncodedContentType,
extra: {'account': account}),
);
return {'status': res.data['code'] == 0, 'msg': res.data['message']};
}
}

View File

@@ -65,11 +65,7 @@ class MemberHttp {
required int mid,
required int page,
}) async {
String? accessKey = GStorage.localCache
.get(LocalCacheKey.accessKey, defaultValue: {})['value'];
Map<String, String> data = {
if (accessKey?.isNotEmpty == true) 'access_key': accessKey!,
'appkey': Constants.appKey,
'build': '1462100',
'c_locale': 'zh_CN',
'channel': 'yingyongbao',
@@ -79,15 +75,8 @@ class MemberHttp {
'ps': '10',
's_locale': 'zh_CN',
'statistics': Constants.statistics,
'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(),
'vmid': mid.toString(),
};
String sign = Utils.appSign(
data,
Constants.appKey,
Constants.appSec,
);
data['sign'] = sign;
dynamic res = await Request().get(
Api.spaceArticle,
queryParameters: data,
@@ -108,11 +97,7 @@ class MemberHttp {
static Future<LoadingState> spaceFav({
required int mid,
}) async {
String? accessKey = GStorage.localCache
.get(LocalCacheKey.accessKey, defaultValue: {})['value'];
Map<String, String> data = {
if (accessKey?.isNotEmpty == true) 'access_key': accessKey!,
'appkey': Constants.appKey,
'build': '1462100',
'c_locale': 'zh_CN',
'channel': 'yingyongbao',
@@ -120,15 +105,8 @@ class MemberHttp {
'platform': 'android',
's_locale': 'zh_CN',
'statistics': Constants.statistics,
'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(),
'up_mid': mid.toString(),
};
String sign = Utils.appSign(
data,
Constants.appKey,
Constants.appSec,
);
data['sign'] = sign;
dynamic res = await Request().get(
Api.spaceFav,
queryParameters: data,
@@ -176,12 +154,8 @@ class MemberHttp {
int? seasonId,
int? seriesId,
}) async {
String? accessKey = GStorage.localCache
.get(LocalCacheKey.accessKey, defaultValue: {})['value'];
Map<String, String> data = {
if (accessKey?.isNotEmpty == true) 'access_key': accessKey!,
if (aid != null) 'aid': aid.toString(),
'appkey': Constants.appKey,
'build': '1462100',
'c_locale': 'zh_CN',
'channel': 'yingyongbao',
@@ -197,25 +171,16 @@ class MemberHttp {
if (order != null) 'order': order,
if (sort != null) 'sort': sort,
'statistics': Constants.statistics,
'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(),
'vmid': mid.toString(),
};
String sign = Utils.appSign(
data,
Constants.appKey,
Constants.appSec,
);
data['sign'] = sign;
dynamic res = await Request().get(
type == ContributeType.video
? Api.spaceArchive
: type == ContributeType.charging
? Api.spaceChargingArchive
: type == ContributeType.season
? Api.spaceSeason
: type == ContributeType.series
? Api.spaceSeries
: Api.spaceBangumi,
switch (type) {
ContributeType.video => Api.spaceArchive,
ContributeType.charging => Api.spaceChargingArchive,
ContributeType.season => Api.spaceSeason,
ContributeType.series => Api.spaceSeries,
ContributeType.bangumi => Api.spaceBangumi,
},
queryParameters: data,
options: Options(
headers: {
@@ -234,11 +199,7 @@ class MemberHttp {
static Future<LoadingState> space({
int? mid,
}) async {
String? accessKey = GStorage.localCache
.get(LocalCacheKey.accessKey, defaultValue: {})['value'];
Map<String, String> data = {
if (accessKey?.isNotEmpty == true) 'access_key': accessKey!,
'appkey': Constants.appKey,
'build': '1462100',
'c_locale': 'zh_CN',
'channel': 'yingyongbao',
@@ -246,15 +207,8 @@ class MemberHttp {
'platform': 'android',
's_locale': 'zh_CN',
'statistics': Constants.statistics,
'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(),
'vmid': mid.toString(),
};
String sign = Utils.appSign(
data,
Constants.appKey,
Constants.appSec,
);
data['sign'] = sign;
dynamic res = await Request().get(
Api.space,
queryParameters: data,

View File

@@ -3,6 +3,7 @@ import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart';
import 'package:PiliPlus/grpc/grpc_repo.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/video/reply/item.dart';
import 'package:PiliPlus/utils/accounts/account.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:dio/dio.dart';
@@ -13,7 +14,8 @@ import 'api.dart';
import 'init.dart';
class ReplyHttp {
static Options get _options => Options(extra: {'clearCookie': true});
static Options get _options =>
Options(extra: {'account': AnonymousAccount()});
static RegExp replyRegExp =
RegExp(GStorage.banWordForReply, caseSensitive: false);

View File

@@ -18,9 +18,7 @@ import '../models/video_detail_res.dart';
import '../utils/id_utils.dart';
import '../utils/recommend_filter.dart';
import '../utils/storage.dart';
import '../utils/utils.dart';
import '../utils/wbi_sign.dart';
import '../pages/mine/controller.dart';
import 'api.dart';
import 'init.dart';
import 'login.dart';
@@ -71,15 +69,8 @@ class VideoHttp {
}
// 添加额外的loginState变量模拟未登录状态
static Future<LoadingState> rcmdVideoListApp(
{bool loginStatus = true, required int freshIdx}) async {
static Future<LoadingState> rcmdVideoListApp({required int freshIdx}) async {
Map<String, String> data = {
'access_key': loginStatus
? (GStorage.localCache
.get(LocalCacheKey.accessKey, defaultValue: {})['value'] ??
'')
: '',
'appkey': Constants.appKey,
'build': '1462100',
'c_locale': 'zh_CN',
'channel': 'yingyongbao',
@@ -106,16 +97,8 @@ class VideoHttp {
's_locale': 'zh_CN',
'splash_id': '',
'statistics': Constants.statistics,
'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(),
'voice_balance': '0'
};
String sign = Utils.appSign(
data,
Constants.appKey,
Constants.appSec,
);
data['sign'] = sign;
var res = await Request().get(
Api.recommendListApp,
queryParameters: data,
@@ -227,24 +210,22 @@ class VideoHttp {
'qn': qn ?? 80,
// 获取所有格式的视频
'fnval': 4048,
};
// 免登录查看1080p
if ((GStorage.userInfo.get('userInfoCache') == null ||
MineController.anonymity.value) &&
GStorage.setting.get(SettingBoxKey.p1080, defaultValue: true)) {
data['try_look'] = 1;
}
Map params = await WbiSign.makSign({
...data,
'fourk': 1,
'voice_balance': 1,
'gaia_source': 'pre-load',
'web_location': 1550101,
});
};
late final usePgcApi = forcePgcApi == true || GStorage.isLogin;
// 免登录查看1080p
if ((Accounts.get(AccountType.video).isLogin) &&
GStorage.setting.get(SettingBoxKey.p1080, defaultValue: true)) {
data['try_look'] = 1;
}
Map params = await WbiSign.makSign(data);
late final usePgcApi =
forcePgcApi == true || Accounts.get(AccountType.video).isLogin;
try {
var res = await Request().get(
@@ -326,7 +307,7 @@ class VideoHttp {
data: {
'platform': 'web',
'season_id': seasonId,
'csrf': await Request.getCsrf(),
'csrf': Accounts.main.csrf,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
@@ -427,14 +408,12 @@ class VideoHttp {
}) async {
var res = await Request().post(
Api.coinVideo,
queryParameters: {
'aid': IdUtils.bv2av(bvid),
data: {
'aid': IdUtils.bv2av(bvid).toString(),
// 'bvid': bvid,
'multiply': multiply,
'select_like': selectLike,
'access_key': GStorage.localCache
.get(LocalCacheKey.accessKey, defaultValue: {})['value'],
// 'csrf': await Request.getCsrf(),
'multiply': multiply.toString(),
'select_like': selectLike.toString(),
// 'csrf': Accounts.main.csrf,
},
);
if (res.data['code'] == 0) {
@@ -461,7 +440,7 @@ class VideoHttp {
Api.triple,
data: {
'ep_id': epId,
'csrf': await Request.getCsrf(),
'csrf': Accounts.main.csrf,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
@@ -489,7 +468,7 @@ class VideoHttp {
'ramval': 0,
'source': 'web_normal',
'ga': 1,
'csrf': await Request.getCsrf(),
'csrf': Accounts.main.csrf,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
@@ -509,18 +488,19 @@ class VideoHttp {
// (取消)点赞
static Future likeVideo({required String bvid, required bool type}) async {
var res = await Request().post(Api.likeVideo, queryParameters: {
'aid': IdUtils.bv2av(bvid),
'like': type ? 0 : 1,
'access_key': GStorage.localCache
.get(LocalCacheKey.accessKey, defaultValue: {})['value'],
}
// queryParameters: {
// 'bvid': bvid,
// 'like': type ? 1 : 2,
// 'csrf': await Request.getCsrf(),
// },
);
var res = await Request().post(
Api.likeVideo,
data: {
'aid': IdUtils.bv2av(bvid).toString(),
'like': type ? '0' : '1',
},
options: Options(contentType: Headers.formUrlEncodedContentType),
// queryParameters: {
// 'bvid': bvid,
// 'like': type ? 1 : 2,
// 'csrf': Accounts.main.csrf,
// },
);
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
@@ -530,17 +510,14 @@ class VideoHttp {
// (取消)点踩
static Future dislikeVideo({required String bvid, required bool type}) async {
String? accessKey = GStorage.localCache
.get(LocalCacheKey.accessKey, defaultValue: {})['value'];
if (accessKey == null || accessKey == "") {
if (Accounts.main.accessKey.isNullOrEmpty) {
return {'status': false, 'msg': "请退出账号后重新登录"};
}
var res = await Request().post(
Api.dislikeVideo,
queryParameters: {
'aid': IdUtils.bv2av(bvid),
'dislike': type ? 0 : 1,
'access_key': accessKey,
data: {
'aid': IdUtils.bv2av(bvid).toString(),
'dislike': type ? '0' : '1',
},
);
if (res.data is! String && res.data['code'] == 0) {
@@ -559,9 +536,7 @@ class VideoHttp {
required int id,
int? reasonId,
int? feedbackId}) async {
String? accessKey = GStorage.localCache
.get(LocalCacheKey.accessKey, defaultValue: {})['value'];
if (accessKey == null || accessKey == "") {
if (Accounts.get(AccountType.recommend).accessKey.isNullOrEmpty) {
return {'status': false, 'msg': "请退出账号后重新登录"};
}
assert((reasonId != null) ^ (feedbackId != null));
@@ -571,10 +546,8 @@ class VideoHttp {
// 'mid': mid,
if (reasonId != null) 'reason_id': reasonId,
if (feedbackId != null) 'feedback_id': feedbackId,
'build': 1,
'build': '1',
'mobi_app': 'android',
'access_key': accessKey,
'appkey': Constants.appKey,
});
if (res.data['code'] == 0) {
return {'status': true};
@@ -589,9 +562,7 @@ class VideoHttp {
required int id,
int? reasonId,
int? feedbackId}) async {
String? accessKey = GStorage.localCache
.get(LocalCacheKey.accessKey, defaultValue: {})['value'];
if (accessKey == null || accessKey == "") {
if (Accounts.get(AccountType.recommend).accessKey.isNullOrEmpty) {
return {'status': false, 'msg': "请退出账号后重新登录"};
}
// assert ((reasonId != null) ^ (feedbackId != null));
@@ -601,10 +572,8 @@ class VideoHttp {
// 'mid': mid,
if (reasonId != null) 'reason_id': reasonId,
if (feedbackId != null) 'feedback_id': feedbackId,
'build': 1,
'build': '1',
'mobi_app': 'android',
'access_key': accessKey,
'appkey': Constants.appKey,
});
if (res.data['code'] == 0) {
return {'status': true};
@@ -624,7 +593,7 @@ class VideoHttp {
'resources': ids?.join(','),
'media_id': delIds,
'platform': 'web',
'csrf': await Request.getCsrf(),
'csrf': Accounts.main.csrf,
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
@@ -649,7 +618,7 @@ class VideoHttp {
'type': type ?? 2,
'add_media_ids': addIds ?? '',
'del_media_ids': delIds ?? '',
'csrf': await Request.getCsrf(),
'csrf': Accounts.main.csrf,
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
@@ -672,7 +641,7 @@ class VideoHttp {
// 'resources': '$epId:24',
// 'add_media_ids': addIds ?? '',
// 'del_media_ids': delIds ?? '',
// 'csrf': await Request.getCsrf(),
// 'csrf': Accounts.main.csrf,
// },
// options: Options(
// headers: {
@@ -709,7 +678,7 @@ class VideoHttp {
if (mid != null) 'mid': mid,
'resources': resources.join(','),
'platform': 'web',
'csrf': await Request.getCsrf(),
'csrf': Accounts.main.csrf,
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
@@ -783,7 +752,7 @@ class VideoHttp {
'message': message,
if (pictures != null) 'pictures': jsonEncode(pictures),
if (syncToDynamic == true) 'sync_to_dynamic': 1,
'csrf': await Request.getCsrf(),
'csrf': Accounts.main.csrf,
};
var res = await Request().post(
Api.replyAdd,
@@ -806,7 +775,7 @@ class VideoHttp {
'type': type, //type.index
'oid': oid,
'rpid': rpid,
'csrf': await Request.getCsrf(),
'csrf': Accounts.main.csrf,
});
log(res.toString());
if (res.data['code'] == 0) {
@@ -842,7 +811,7 @@ class VideoHttp {
"entity_id": mid,
'fp': Request.headerUa(type: 'pc'),
},
'csrf': await Request.getCsrf(),
'csrf': Accounts.main.csrf,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
@@ -878,7 +847,7 @@ class VideoHttp {
if (epid != null) 'type': 4,
if (subType != null) 'sub_type': subType,
'played_time': progress,
'csrf': await Request.getCsrf(),
'csrf': Accounts.main.csrf,
});
}
@@ -891,7 +860,7 @@ class VideoHttp {
'desc': desc,
'oid': oid,
'upper_mid': upperMid,
'csrf': await Request.getCsrf(),
'csrf': Accounts.main.csrf,
});
}
@@ -899,7 +868,7 @@ class VideoHttp {
static Future bangumiAdd({int? seasonId}) async {
var res = await Request().post(Api.bangumiAdd, queryParameters: {
'season_id': seasonId,
'csrf': await Request.getCsrf(),
'csrf': Accounts.main.csrf,
});
if (res.data['code'] == 0) {
return {
@@ -920,7 +889,7 @@ class VideoHttp {
static Future bangumiDel({int? seasonId}) async {
var res = await Request().post(Api.bangumiDel, queryParameters: {
'season_id': seasonId,
'csrf': await Request.getCsrf(),
'csrf': Accounts.main.csrf,
});
if (res.data['code'] == 0) {
return {
@@ -946,7 +915,7 @@ class VideoHttp {
data: {
'season_id': seasonId,
'status': status,
'csrf': await Request.getCsrf(),
'csrf': Accounts.main.csrf,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
@@ -1143,7 +1112,7 @@ class VideoHttp {
var res = await Request().get(
Api.noteList,
queryParameters: {
'csrf': await Request.getCsrf(),
'csrf': Accounts.main.csrf,
'oid': oid,
'oid_type': 0,
'pn': page,

View File

@@ -1,7 +0,0 @@
// 首页推荐类型
enum RcmdType { web, app, notLogin }
extension RcmdTypeExtension on RcmdType {
String get values => ['web', 'app', 'notLogin'][index];
String get labels => ['web端', 'app端', '游客模式'][index];
}

View File

@@ -1,10 +1,9 @@
import 'dart:convert';
import 'package:PiliPlus/build_config.dart';
import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/services/loggeer.dart';
import 'package:PiliPlus/utils/accounts/account.dart';
import 'package:PiliPlus/utils/login.dart';
import 'package:cookie_jar/cookie_jar.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@@ -13,7 +12,6 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:PiliPlus/models/github/latest.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/utils.dart';
import '../../http/init.dart';
import '../../utils/cache_manage.dart';
class AboutPage extends StatefulWidget {
@@ -250,22 +248,8 @@ Commit Hash: ${BuildConfig.commitHash}''',
title: const Text('导出'),
onTap: () async {
Get.back();
dynamic accessKey = GStorage.localCache
.get(LocalCacheKey.accessKey, defaultValue: {});
dynamic cookies = (await Request.cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.baseUrl)))
.map(
(Cookie cookie) => {
'name': cookie.name,
'value': cookie.value,
},
)
.toList();
dynamic res = jsonEncode({
'accessKey': accessKey,
'cookies': cookies,
});
Utils.copyText('$res');
String res = jsonEncode(Accounts.account.toMap());
Utils.copyText(res);
// if (context.mounted) {
// showDialog(
// context: context,
@@ -282,9 +266,7 @@ Commit Hash: ${BuildConfig.commitHash}''',
Get.back();
ClipboardData? data =
await Clipboard.getData('text/plain');
if (data == null ||
data.text == null ||
data.text!.isEmpty) {
if (data?.text?.isNotEmpty != true) {
SmartDialog.showToast('剪贴板无数据');
return;
}
@@ -295,7 +277,7 @@ Commit Hash: ${BuildConfig.commitHash}''',
return AlertDialog(
title: const Text('是否导入以下登录信息?'),
content: SingleChildScrollView(
child: Text(data.text!),
child: Text(data!.text!),
),
actions: [
TextButton(
@@ -309,14 +291,21 @@ Commit Hash: ${BuildConfig.commitHash}''',
),
),
TextButton(
onPressed: () async {
onPressed: () {
Get.back();
try {
dynamic res = jsonDecode(data.text!);
LoginUtils.onLogin(
res['accessKey'],
{'cookies': res['cookies']},
);
final res = (jsonDecode(data.text!)
as Map)
.map((key, value) => MapEntry(key,
LoginAccount.fromJson(value)));
Accounts.account
.putAll(res)
.then((_) => Accounts.refresh())
.then((_) {
if (Accounts.main.isLogin) {
return LoginUtils.onLoginMain();
}
});
} catch (e) {
SmartDialog.showToast('导入失败:$e');
}
@@ -448,6 +437,7 @@ Commit Hash: ${BuildConfig.commitHash}''',
GStorage.localCache.clear(),
GStorage.video.clear(),
GStorage.historyWord.clear(),
Accounts.clear(),
]);
SmartDialog.showToast('重置成功');
},

View File

@@ -2,14 +2,13 @@ import 'dart:convert';
import 'dart:io';
import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart';
import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/reply.dart';
import 'package:PiliPlus/models/common/reply_type.dart';
import 'package:PiliPlus/models/video/reply/data.dart';
import 'package:PiliPlus/pages/common/common_controller.dart';
import 'package:PiliPlus/pages/video/detail/reply_new/reply_page.dart';
import 'package:PiliPlus/utils/accounts/account.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/global_data.dart';
import 'package:PiliPlus/utils/utils.dart';
@@ -31,7 +30,7 @@ abstract class ReplyController extends CommonController {
late final savedReplies = {};
late final bool isLogin = GStorage.userInfo.get('userInfoCache') != null;
late final bool isLogin = Accounts.main.isLogin;
CursorReply? cursor;
late Rx<Mode> mode = Mode.MAIN_LIST_HOT.obs;
@@ -374,10 +373,10 @@ abstract class ReplyController extends CommonController {
// biliSendCommAntifraud
if (Platform.isAndroid && _biliSendCommAntifraud) {
try {
List<Cookie> cookies = await Request.cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.apiBaseUrl));
final String cookieString = cookies
.map((Cookie cookie) => '${cookie.name}=${cookie.value}')
final String cookieString = Accounts.main.cookieJar
.toJson()
.entries
.map((i) => '${i.key}=${i.value}')
.join(';');
Utils.channel.invokeMethod(
'biliSendCommAntifraud',

View File

@@ -312,7 +312,7 @@ class AuthorPanel extends StatelessWidget {
},
minLeadingWidth: 0,
),
if (GStorage.isLogin)
if (Accounts.main.isLogin)
ListTile(
title: Text(
'举报',

View File

@@ -25,7 +25,7 @@ class LiveController extends CommonController {
return super.onRefresh();
}
late RxBool isLogin = GStorage.isLogin.obs;
late RxBool isLogin = Accounts.main.isLogin.obs;
late Rx<LoadingState> followListState = LoadingState.loading().obs;
late int followPage = 1;
late bool followEnd = false;

View File

@@ -3,9 +3,9 @@ import 'dart:io';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/icon_button.dart';
import 'package:PiliPlus/common/widgets/radio_widget.dart';
import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/login.dart';
import 'package:PiliPlus/utils/accounts/account.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
@@ -84,8 +84,8 @@ class LoginPageController extends GetxController
if (value['status']) {
t.cancel();
statusQRCode.value = '扫码成功';
await LoginUtils.onLogin(
value['data'], value['data']['cookie_info']);
await setAccount(
value['data'], value['data']['cookie_info']['cookies']);
Get.back();
} else if (value['code'] == 86038) {
t.cancel();
@@ -212,31 +212,27 @@ class LoginPageController extends GetxController
}
try {
dynamic result = await Request().get(
"https://api.bilibili.com/x/member/web/account",
options: Options(
headers: {
"Cookie": cookieTextController.text,
},
),
"/x/member/web/account",
options: Options(headers: {
"cookie": cookieTextController.text,
}, extra: {
'account': AnonymousAccount()
}),
);
if (result.data['code'] == 0) {
try {
await LoginUtils.onLogin(
{'mid': '${result.data['data']['mid']}'},
{
'cookies':
cookieTextController.text.split(';').toList().map((item) {
List list = item.split('=').toList();
return {
'name': list.firstOrNull,
'value': list.getOrNull(1),
};
}).toList()
},
);
if (GStorage.isLogin) {
Get.back();
}
await LoginAccount(
BiliCookieJar.fromJson(Map.fromEntries(
cookieTextController.text.split(';').map((item) {
final list = item.split('=');
return MapEntry(list.first, list.skip(1).join());
}))),
null,
null)
.onChange();
if (!Accounts.main.isLogin) await switchAccountDialog(Get.context!);
SmartDialog.showToast('登录成功');
Get.back();
} catch (e) {
SmartDialog.showToast("登录失败: $e");
}
@@ -434,8 +430,8 @@ class LoginPageController extends GetxController
return;
}
SmartDialog.showToast('正在保存身份信息');
await LoginUtils.onLogin(
data['token_info'], data['cookie_info']);
await setAccount(
data['token_info'], data['cookie_info']['cookies']);
Get.back();
Get.back();
},
@@ -453,7 +449,7 @@ class LoginPageController extends GetxController
return;
}
SmartDialog.showToast('正在保存身份信息');
await LoginUtils.onLogin(data['token_info'], data['cookie_info']);
await setAccount(data['token_info'], data['cookie_info']['cookies']);
Get.back();
} else {
// handle login result
@@ -516,7 +512,7 @@ class LoginPageController extends GetxController
if (res['status']) {
SmartDialog.showToast('登录成功');
var data = res['data'];
await LoginUtils.onLogin(data['token_info'], data['cookie_info']);
await setAccount(data['token_info'], data['cookie_info']['cookies']);
Get.back();
} else {
SmartDialog.showToast(res['msg']);
@@ -644,4 +640,82 @@ class LoginPageController extends GetxController
geeChallenge?.isNotEmpty == true &&
captchaData.token?.isNotEmpty == true;
}
Future<void> setAccount(Map tokenInfo, List cookieInfo) async {
await Future.wait([
LoginAccount(BiliCookieJar.fromList(cookieInfo),
tokenInfo['access_token'], tokenInfo['refresh_token'])
.onChange(),
AnonymousAccount().logout().then((i) => Request.buvidActive(i))
]);
if (Accounts.main.isLogin) {
SmartDialog.showToast('登录成功');
} else {
SmartDialog.showToast('登录成功, 请先设置账号模式');
await switchAccountDialog(Get.context!);
}
}
static Future switchAccountDialog(BuildContext context) {
if (Accounts.account.isEmpty) {
return SmartDialog.showToast('请先登录');
}
final selectAccount = Accounts.accountMode
.map((key, value) => MapEntry(key, value.mid.toString()));
final options = {'0': '0', for (String i in Accounts.account.keys) i: i};
return showDialog(
context: context,
builder: (context) => StatefulBuilder(builder: (context, setState) {
return AlertDialog(
title: const Text('选择账号mid, 为0时使用匿名'),
titlePadding: const EdgeInsets.only(left: 22, top: 16, right: 22),
contentPadding: const EdgeInsets.symmetric(vertical: 5),
actionsPadding: const EdgeInsets.only(
left: 16,
right: 16,
bottom: 10,
),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: AccountType.values
.map(
(e) => WrapRadioOptionsGroup<String>(
groupTitle: e.title,
options: options,
selectedValue: selectAccount[e],
onChanged: (v) => setState(() => selectAccount[e] = v!),
),
)
.toList(),
),
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
),
TextButton(
onPressed: () {
for (var i in selectAccount.entries) {
var account =
Accounts.account.get(i.value) ?? AnonymousAccount();
if (account != Accounts.get(i.key)) {
Accounts.set(i.key, account);
}
}
Get.back();
},
child: const Text('确定'),
),
],
);
}),
);
}
}

View File

@@ -49,7 +49,7 @@ class MainController extends GetxController {
}
hideTabBar =
GStorage.setting.get(SettingBoxKey.hideTabBar, defaultValue: true);
isLogin.value = GStorage.isLogin;
isLogin.value = Accounts.main.isLogin;
dynamicBadgeMode = DynamicBadgeMode.values[GStorage.setting.get(
SettingBoxKey.dynamicBadgeMode,
defaultValue: DynamicBadgeMode.number.index)];

View File

@@ -4,7 +4,6 @@ import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/http/index.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dio/dio.dart';
@@ -54,10 +53,6 @@ class _EditProfilePageState extends State<EditProfilePage> {
_getInfo() async {
Map<String, String> data = {
'access_key': GStorage.localCache
.get(LocalCacheKey.accessKey, defaultValue: {})['value'] ??
'',
'appkey': Constants.appKey,
'build': '1462100',
'c_locale': 'zh_CN',
'channel': 'yingyongbao',
@@ -65,19 +60,10 @@ class _EditProfilePageState extends State<EditProfilePage> {
'platform': 'android',
's_locale': 'zh_CN',
'statistics': Constants.statistics,
'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(),
};
String sign = Utils.appSign(
data,
Constants.appKey,
Constants.appSec,
);
data['sign'] = sign;
Request()
.get(
'${HttpString.appBaseUrl}/x/v2/account/myinfo',
queryParameters: data,
)
.get('${HttpString.appBaseUrl}/x/v2/account/myinfo',
queryParameters: data)
.then((data) {
setState(() {
if (data.data['code'] == 0) {
@@ -329,10 +315,6 @@ class _EditProfilePageState extends State<EditProfilePage> {
dynamic datum,
}) async {
Map<String, String> data = {
'access_key': GStorage.localCache
.get(LocalCacheKey.accessKey, defaultValue: {})['value'] ??
'',
'appkey': Constants.appKey,
'build': '1462100',
'c_locale': 'zh_CN',
'channel': 'yingyongbao',
@@ -350,12 +332,6 @@ class _EditProfilePageState extends State<EditProfilePage> {
else if (type == ProfileType.sex)
'sex': datum.toString(),
};
String sign = Utils.appSign(
data,
Constants.appKey,
Constants.appSec,
);
data['sign'] = sign;
Request()
.post(
'/x/member/app/${type.name}/update',

View File

@@ -24,7 +24,7 @@ class MemberCoinPage extends StatefulWidget {
}
class _MemberCoinPageState extends State<MemberCoinPage> {
late final _ownerMid = GStorage.ownerMid;
late final _ownerMid = Accounts.main.mid;
late final _ctr = Get.put(
MemberCoinController(mid: widget.mid),

View File

@@ -24,7 +24,7 @@ class MemberLikePage extends StatefulWidget {
}
class _MemberLikePageState extends State<MemberLikePage> {
late final _ownerMid = GStorage.ownerMid;
late final _ownerMid = Accounts.main.mid;
late final _ctr = Get.put(
MemberLikeController(mid: widget.mid),

View File

@@ -1,3 +1,4 @@
import 'package:PiliPlus/utils/accounts/account.dart';
import 'package:PiliPlus/utils/login.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@@ -20,8 +21,9 @@ class MineController extends GetxController {
Rx<ThemeType> themeType = ThemeType.system.obs;
static Box get setting => GStorage.setting;
static RxBool anonymity =
(setting.get(SettingBoxKey.anonymity, defaultValue: false) as bool).obs;
static RxBool anonymity = (Accounts.account.isNotEmpty &&
!Accounts.get(AccountType.heartbeat).isLogin)
.obs;
ThemeType get nextThemeType =>
ThemeType.values[(themeType.value.index + 1) % ThemeType.values.length];
@@ -36,8 +38,8 @@ class MineController extends GetxController {
}
}
onLogin() async {
if (!isLogin.value) {
onLogin([bool longPress = false]) async {
if (!isLogin.value || longPress) {
Get.toNamed('/loginPage', preventDuplicates: false);
} else {
int mid = userInfo.value.mid!;
@@ -58,13 +60,13 @@ class MineController extends GetxController {
GStorage.userInfo.put('userInfoCache', res['data']);
isLogin.value = true;
} else {
LoginUtils.onLogout();
LoginUtils.onLogoutMain();
return;
}
} else {
SmartDialog.showToast(res['msg']);
if (res['msg'] == '账号未登录') {
LoginUtils.onLogout();
LoginUtils.onLogoutMain();
return;
}
}
@@ -79,8 +81,13 @@ class MineController extends GetxController {
}
static onChangeAnonymity(BuildContext context) {
if (Accounts.account.isEmpty) {
SmartDialog.showToast('请先登录');
return;
}
anonymity.value = !anonymity.value;
if (anonymity.value) {
Accounts.accountMode[AccountType.heartbeat] = AnonymousAccount();
SmartDialog.show(
clickMaskDismiss: false,
usePenetrate: true,
@@ -122,8 +129,8 @@ class MineController extends GetxController {
TextButton(
onPressed: () {
SmartDialog.dismiss();
setting.put(SettingBoxKey.anonymity, true);
anonymity.value = true;
Accounts.set(
AccountType.heartbeat, AnonymousAccount());
SmartDialog.showToast('已设为永久无痕模式');
},
child: Text(
@@ -136,8 +143,6 @@ class MineController extends GetxController {
TextButton(
onPressed: () {
SmartDialog.dismiss();
setting.put(SettingBoxKey.anonymity, false);
anonymity.value = true;
SmartDialog.showToast('已设为临时无痕模式');
},
child: Text(
@@ -158,7 +163,7 @@ class MineController extends GetxController {
},
);
} else {
setting.put(SettingBoxKey.anonymity, false);
Accounts.set(AccountType.heartbeat, Accounts.main);
SmartDialog.show(
clickMaskDismiss: false,
usePenetrate: true,

View File

@@ -129,6 +129,7 @@ class _MinePageState extends State<MinePage> {
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _mineController.onLogin,
onLongPress: () => _mineController.onLogin(true),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [

View File

@@ -6,7 +6,7 @@ import 'package:PiliPlus/utils/storage.dart';
class RcmdController extends CommonController {
late bool enableSaveLastData = GStorage.setting
.get(SettingBoxKey.enableSaveLastData, defaultValue: false);
late String defaultRcmdType = 'app';
late bool appRcmd = true;
int? lastRefreshAt;
late bool savedRcmdTip = GStorage.savedRcmdTip;
@@ -14,8 +14,7 @@ class RcmdController extends CommonController {
@override
void onInit() {
super.onInit();
defaultRcmdType = GStorage.setting
.get(SettingBoxKey.defaultRcmdType, defaultValue: 'app');
appRcmd = GStorage.appRcmd;
currentPage = 0;
queryData();
@@ -23,15 +22,9 @@ class RcmdController extends CommonController {
@override
Future<LoadingState> customGetData() {
return defaultRcmdType == 'app' || defaultRcmdType == 'notLogin'
? VideoHttp.rcmdVideoListApp(
loginStatus: defaultRcmdType != 'notLogin',
freshIdx: currentPage,
)
: VideoHttp.rcmdVideoList(
freshIdx: currentPage,
ps: 20,
);
return appRcmd
? VideoHttp.rcmdVideoListApp(freshIdx: currentPage)
: VideoHttp.rcmdVideoList(freshIdx: currentPage, ps: 20);
}
@override

View File

@@ -16,9 +16,7 @@ class RecommendSetting extends StatelessWidget {
ListTile(
dense: true,
subtitle: Text(
'¹ 若默认web端推荐不太符合预期可尝试切换至app端。\n'
'¹ 选择“游客模式(notLogin)”将以空的key请求app推荐接口但播放页仍会携带用户信息保证账号能正常记录进度、点赞投币等。\n\n'
'² 由于接口未提供关注信息无法豁免相关视频中的已关注Up。\n\n'
'¹ 由于接口未提供关注信息无法豁免相关视频中的已关注Up。\n\n'
'* 其它(如热门视频、手动搜索、链接跳转等)均不受过滤器影响。\n'
'* 设定较严苛的条件可导致推荐项数锐减或多次请求,请酌情选择。\n'
'* 后续可能会增加更多过滤条件,敬请期待。',

View File

@@ -8,7 +8,6 @@ import 'package:PiliPlus/models/common/sponsor_block/skip_type.dart';
import 'package:PiliPlus/pages/setting/slide_color_picker.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
@@ -66,7 +65,6 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
Request()
.get(
'$_blockServer/api/status/uptime',
options: Options(extra: {'clearCookie': true}),
)
.then((res) {
setState(() {

View File

@@ -1,5 +1,6 @@
import 'package:PiliPlus/build_config.dart';
import 'package:PiliPlus/http/login.dart';
import 'package:PiliPlus/pages/about/index.dart';
import 'package:PiliPlus/pages/login/controller.dart';
import 'package:PiliPlus/pages/setting/extra_setting.dart';
import 'package:PiliPlus/pages/setting/play_setting.dart';
import 'package:PiliPlus/pages/setting/privacy_setting.dart';
@@ -9,13 +10,10 @@ import 'package:PiliPlus/pages/setting/video_setting.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/login.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import '../../http/init.dart';
class _SettingsModel {
final String name;
final String title;
@@ -39,7 +37,7 @@ class SettingPage extends StatefulWidget {
class _SettingPageState extends State<SettingPage> {
late String _type = 'privacySetting';
final RxBool _isLogin = GStorage.isLogin.obs;
final RxBool _isLogin = Accounts.main.isLogin.obs;
TextStyle get _titleStyle => Theme.of(context).textTheme.titleMedium!;
TextStyle get _subTitleStyle => Theme.of(context)
.textTheme
@@ -170,6 +168,12 @@ class _SettingPageState extends State<SettingPage> {
: Text(item.subtitle!, style: _subTitleStyle),
),
),
ListTile(
onTap: () => LoginPageController.switchAccountDialog(context),
leading: const Icon(Icons.switch_account_outlined),
title: const Text('设置账号模式'),
),
// TODO: 多账号登出
_buildLoginItem,
ListTile(
tileColor: _getTileColor(_items.last.name),
@@ -204,41 +208,36 @@ class _SettingPageState extends State<SettingPage> {
),
),
),
if (BuildConfig.isDebug)
TextButton(
onPressed: () {
Get.back();
_isLogin.value = false;
LoginUtils.onLogout();
},
child: Text(
'仅登出',
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
TextButton(
onPressed: () {
Get.back();
_isLogin.value = false;
LoginUtils.onLogoutMain();
final account = Accounts.main;
Accounts.accountMode
.removeWhere((_, a) => a == account);
account.logout().then((_) => Accounts.refresh());
},
child: Text(
'仅登出',
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
),
TextButton(
onPressed: () async {
SmartDialog.showLoading();
dynamic res = await Request().post(
'https://passport.bilibili.com/login/exit/v2',
data: {
'biliCSRF': await Request.getCsrf(),
},
options: Options(
contentType:
Headers.formUrlEncodedContentType,
),
);
if (res.data['code'] == 0) {
await LoginUtils.onLogout();
final res = await LoginHttp.logout(Accounts.main);
if (res['status']) {
await Accounts.main.logout();
await LoginUtils.onLogoutMain();
_isLogin.value = false;
SmartDialog.dismiss();
Get.back();
} else {
SmartDialog.dismiss();
SmartDialog.showToast('${res.data['message']}');
SmartDialog.showToast(res['msg'].toString());
}
},
child: const Text('确认'),

View File

@@ -3,7 +3,6 @@ import 'dart:math';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart'
show kDragContainerExtentPercentage, displacement;
import 'package:PiliPlus/http/interceptor.dart';
import 'package:PiliPlus/http/reply.dart';
import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/main.dart';
@@ -11,7 +10,6 @@ import 'package:PiliPlus/models/common/audio_normalization.dart';
import 'package:PiliPlus/models/common/dynamic_badge_mode.dart';
import 'package:PiliPlus/models/common/dynamics_type.dart';
import 'package:PiliPlus/models/common/nav_bar_config.dart';
import 'package:PiliPlus/models/common/rcmd_type.dart';
import 'package:PiliPlus/models/common/reply_sort_type.dart';
import 'package:PiliPlus/models/common/super_resolution_type.dart';
import 'package:PiliPlus/models/common/theme_type.dart';
@@ -35,6 +33,7 @@ import 'package:PiliPlus/pages/setting/widgets/switch_item.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/plugin/pl_player/utils/fullscreen.dart';
import 'package:PiliPlus/utils/accounts/account_manager/account_mgr.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/feed_back.dart';
import 'package:PiliPlus/utils/global_data.dart';
@@ -1229,35 +1228,19 @@ List<SettingsModel> get videoSettings => [
List<SettingsModel> get recommendSettings => [
SettingsModel(
settingsType: SettingsType.normal,
title: '首页推荐类型',
leading: const Icon(Icons.model_training_outlined),
getSubtitle: () => '当前使用「${GStorage.defaultRcmdType}端」推荐¹',
onTap: (setState) async {
String? result = await showDialog(
context: Get.context!,
builder: (context) {
return SelectDialog<String>(
title: '推荐类型',
value: GStorage.defaultRcmdType,
values: RcmdType.values.map((e) {
return {'title': e.labels, 'value': e.values};
}).toList(),
);
},
);
if (result != null) {
if (result == 'app') {
if (GStorage.isLogin.not) {
SmartDialog.showToast('尚未登录,无法收到个性化推荐');
}
settingsType: SettingsType.sw1tch,
title: '首页使用app端推荐',
subtitle: '若web端推荐不太符合预期可尝试切换至app端推荐',
leading: const Icon(Icons.model_training_outlined),
setKey: SettingBoxKey.appRcmd,
defaultVal: true,
onChanged: (value) {
try {
Get.find<RcmdController>().appRcmd = value;
} catch (e) {
debugPrint('$e');
}
await GStorage.setting.put(SettingBoxKey.defaultRcmdType, result);
SmartDialog.showToast('下次启动时生效');
setState();
}
},
),
}),
SettingsModel(
settingsType: SettingsType.sw1tch,
title: '推荐动态',
@@ -1430,7 +1413,7 @@ List<SettingsModel> get recommendSettings => [
SettingsModel(
settingsType: SettingsType.sw1tch,
title: '过滤器也应用于相关视频',
subtitle: '视频详情页的相关视频也进行过滤²',
subtitle: '视频详情页的相关视频也进行过滤¹',
leading: const Icon(Icons.explore_outlined),
setKey: SettingBoxKey.applyFilterToRelatedVideos,
defaultVal: true,
@@ -1442,7 +1425,7 @@ List<SettingsModel> get privacySettings => [
SettingsModel(
settingsType: SettingsType.normal,
onTap: (setState) {
if (GStorage.isLogin.not) {
if (Accounts.main.isLogin.not) {
SmartDialog.showToast('登录后查看');
return;
}
@@ -1472,7 +1455,8 @@ List<SettingsModel> get privacySettings => [
builder: (context) {
return AlertDialog(
title: const Text('查看详情'),
content: Text(ApiInterceptor.anonymityList.join('\n')),
content: Text(AccountManager.apiTypeSet[AccountType.heartbeat]!
.join('\n')),
actions: [
TextButton(
onPressed: () async {
@@ -1501,12 +1485,9 @@ List<SettingsModel> get extraSettings => [
onTap: () => Get.toNamed('/sponsorBlock'),
leading: Stack(
alignment: Alignment.center,
children: [
const Icon(Icons.shield_outlined),
Icon(
Icons.play_arrow_rounded,
size: 15,
),
children: const [
Icon(Icons.shield_outlined),
Icon(Icons.play_arrow_rounded, size: 15),
],
),
),
@@ -2056,9 +2037,9 @@ List<SettingsModel> get extraSettings => [
subtitle: '发送评论后检查评论是否可见',
leading: Stack(
alignment: Alignment.center,
children: [
const Icon(Icons.shield_outlined),
const Icon(Icons.reply, size: 14),
children: const [
Icon(Icons.shield_outlined),
Icon(Icons.reply, size: 14),
],
),
setKey: SettingBoxKey.enableCommAntifraud,
@@ -2081,12 +2062,9 @@ List<SettingsModel> get extraSettings => [
subtitle: '发布/转发动态后检查动态是否可见',
leading: Stack(
alignment: Alignment.center,
children: [
const Icon(Icons.shield_outlined),
Icon(
Icons.motion_photos_on,
size: 12,
),
children: const [
Icon(Icons.shield_outlined),
Icon(Icons.motion_photos_on, size: 12),
],
),
setKey: SettingBoxKey.enableCreateDynAntifraud,
@@ -2097,7 +2075,7 @@ List<SettingsModel> get extraSettings => [
title: '屏蔽带货动态',
leading: Stack(
alignment: Alignment.center,
children: [
children: const [
Icon(Icons.shopping_bag_outlined, size: 14),
Icon(Icons.not_interested),
],
@@ -2113,7 +2091,7 @@ List<SettingsModel> get extraSettings => [
title: '屏蔽带货评论',
leading: Stack(
alignment: Alignment.center,
children: [
children: const [
Icon(Icons.shopping_bag_outlined, size: 14),
Icon(Icons.not_interested),
],

View File

@@ -1,9 +1,8 @@
import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/models/video/play/CDN.dart';
import 'package:PiliPlus/models/video/play/url.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/id_utils.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/video_utils.dart';
import 'package:dio/dio.dart';
@@ -46,23 +45,12 @@ class _SelectDialogState<T> extends State<SelectDialog<T>> {
if (result['status']) {
VideoItem videoItem = result['data'].dash.video.first;
late final isLogin = GStorage.isLogin;
late final dynamic mid =
GStorage.userInfo.get('userInfoCache')?.mid;
for (CDNService item in CDNService.values) {
if (mounted.not) {
break;
}
String videoUrl = VideoUtils.getCdnUrl(videoItem, item.code);
Dio dio = Dio()
..options.headers['referer'] = 'https://www.bilibili.com/';
if (isLogin) {
dio.interceptors.add(Request.cookieManager);
dio.options.headers['x-bili-mid'] = mid;
dio.options.headers['x-bili-aurora-eid'] =
IdUtils.genAuroraEid(mid);
}
Dio dio = Dio()..options.headers['referer'] = HttpString.baseUrl;
int maxSize = 8 * 1024 * 1024;
int downloaded = 0;
int start = DateTime.now().millisecondsSinceEpoch;

View File

@@ -26,7 +26,6 @@ import 'package:PiliPlus/pages/video/detail/widgets/send_danmaku_panel.dart';
import 'package:PiliPlus/pages/video/detail/widgets/media_list_panel.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:floating/floating.dart';
@@ -493,17 +492,14 @@ class VideoDetailController extends GetxController
late final List listData = [];
Future _vote(String uuid, int type) async {
Request()
.post(
Request().post(
'${GStorage.blockServer}/api/voteOnSponsorTime',
queryParameters: {
'UUID': uuid,
'userID': GStorage.blockUserID,
'type': type,
},
options: options,
)
.then((res) {
).then((res) {
SmartDialog.showToast(res.statusCode == 200 ? '投票成功' : '投票失败');
});
}
@@ -522,17 +518,14 @@ class VideoDetailController extends GetxController
dense: true,
onTap: () {
Get.back();
Request()
.post(
Request().post(
'${GStorage.blockServer}/api/voteOnSponsorTime',
queryParameters: {
'UUID': segment.UUID,
'userID': GStorage.blockUserID,
'category': item.name,
},
options: options,
)
.then((res) {
).then((res) {
SmartDialog.showToast(
'类别更改${res.statusCode == 200 ? '成功' : '失败'}');
});
@@ -725,8 +718,6 @@ class VideoDetailController extends GetxController
);
}
Options get options => Options(extra: {'clearCookie': true});
Future _querySponsorBlock() async {
positionSubscription?.cancel();
videoLabel.value = '';
@@ -738,7 +729,6 @@ class VideoDetailController extends GetxController
'videoID': bvid,
'cid': cid.value,
},
options: options,
);
handleSBData(result);
}
@@ -940,7 +930,6 @@ class VideoDetailController extends GetxController
Request().post(
'${GStorage.blockServer}/api/viewedVideoSponsorTime',
queryParameters: {'UUID': item.UUID},
options: options,
);
}
} catch (e) {

View File

@@ -330,8 +330,7 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
TextButton(
onPressed: () {
Get.back();
Request()
.post(
Request().post(
'${GStorage.blockServer}/api/skipSegments',
queryParameters: {
'videoID': videoDetailController.bvid,
@@ -355,9 +354,7 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
)
.toList(),
},
options: videoDetailController.options,
)
.then(
).then(
(res) {
if (res.statusCode == 200) {
Get.back();

View File

@@ -1048,7 +1048,7 @@ class ReplyItem extends StatelessWidget {
'/x/v2/reply/report',
data: {
'add_blacklist': banUid,
'csrf': await Request.getCsrf(),
'csrf': Accounts.main.csrf,
'gaia_source': 'main_h5',
'oid': item.oid,
'platform': 'android',
@@ -1135,7 +1135,7 @@ class ReplyItem extends StatelessWidget {
}
}
dynamic ownerMid = GStorage.ownerMid;
int ownerMid = Accounts.main.mid;
Color errorColor = Theme.of(context).colorScheme.error;
return Padding(
@@ -1168,7 +1168,7 @@ class ReplyItem extends StatelessWidget {
),
),
),
if (ownerMid != null) ...[
if (ownerMid != 0) ...[
ListTile(
onTap: () => menuActionHandler('delete'),
minLeadingWidth: 0,

View File

@@ -1177,7 +1177,7 @@ class ReplyItemGrpc extends StatelessWidget {
}
}
dynamic ownerMid = GStorage.ownerMid;
int ownerMid = Accounts.main.mid;
Color errorColor = Theme.of(context).colorScheme.error;
return Padding(
@@ -1210,7 +1210,7 @@ class ReplyItemGrpc extends StatelessWidget {
),
),
),
if (ownerMid != null) ...[
if (ownerMid != 0) ...[
ListTile(
onTap: () => menuActionHandler('delete'),
minLeadingWidth: 0,

View File

@@ -1,16 +1,16 @@
import 'dart:async';
import 'dart:io';
import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/utils/accounts/account.dart';
import 'package:PiliPlus/utils/app_scheme.dart';
import 'package:PiliPlus/utils/cache_manage.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:cookie_jar/cookie_jar.dart' as cookie_jar;
enum _WebviewMenuItem {
refresh,
@@ -127,10 +127,8 @@ class _WebviewPageNewState extends State<WebviewPageNew> {
}
break;
case _WebviewMenuItem.resetCookie:
final List<cookie_jar.Cookie> cookies = await Request
.cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.baseUrl));
for (cookie_jar.Cookie item in cookies) {
final cookies = Accounts.main.cookieJar.toList();
for (var item in cookies) {
await CookieManager().setCookie(
url: WebUri(item.domain ?? ''),
name: item.name,

View 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()),
},
};
}

View 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;
}

View 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.

View File

@@ -0,0 +1,90 @@
# dio_cookie_manager
[![Pub](https://img.shields.io/pub/v/dio_cookie_manager.svg)](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)!,
);
```

View 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];
}

View 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;
}

View 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;
}

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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];
}

View File

@@ -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) {