diff --git a/lib/common/widgets/list_sheet.dart b/lib/common/widgets/list_sheet.dart index 1ff018bf2..ac018314a 100644 --- a/lib/common/widgets/list_sheet.dart +++ b/lib/common/widgets/list_sheet.dart @@ -131,7 +131,7 @@ class _ListSheetContentState extends CommonSlidePageState 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(); () async { dynamic result = await VideoHttp.videoRelation(bvid: widget.bvid); diff --git a/lib/common/widgets/video_popup_menu.dart b/lib/common/widgets/video_popup_menu.dart index e7218c3e7..51ef73690 100644 --- a/lib/common/widgets/video_popup_menu.dart +++ b/lib/common/widgets/video_popup_menu.dart @@ -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; diff --git a/lib/grpc/grpc_repo.dart b/lib/grpc/grpc_repo.dart index 6e9031530..683549088 100644 --- a/lib/grpc/grpc_repo.dart +++ b/lib/grpc/grpc_repo.dart @@ -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', diff --git a/lib/http/api.dart b/lib/http/api.dart index 2f9897498..8211d9a52 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -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'; diff --git a/lib/http/dynamics.dart b/lib/http/dynamics.dart index f7a469f8d..c9fcc78ce 100644 --- a/lib/http/dynamics.dart +++ b/lib/http/dynamics.dart @@ -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 { diff --git a/lib/http/init.dart b/lib/http/init.dart index 4250d0410..80c37e4c0 100644 --- a/lib/http/init.dart +++ b/lib/http/init.dart @@ -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''); /// 设置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 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 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 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 getCsrf() async { - List 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 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.generate(32, (_) => _rand.nextInt(256)) + + List.filled(4, 0) + + [73, 69, 78, 68] + + List.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.generate(32, (_) => rand.nextInt(256)) + - List.filled(4, 0) + - [73, 69, 78, 68] + - List.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)); } } diff --git a/lib/http/interceptor.dart b/lib/http/interceptor.dart deleted file mode 100644 index c8f3c22fc..000000000 --- a/lib/http/interceptor.dart +++ /dev/null @@ -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 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 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, - // {'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 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]; -} diff --git a/lib/http/login.dart b/lib/http/login.dart index fe1b7ab0d..2700080a5 100644 --- a/lib/http/login.dart +++ b/lib/http/login.dart @@ -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 headers = { - 'Host': host, 'buvid': buvid, 'env': 'prod', 'app-key': 'android_hd', @@ -27,21 +26,14 @@ class LoginHttp { static Future> 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 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(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 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(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(key, value); - }); + Utils.appSign(data); var res = await Request().post( Api.oauth2AccessToken, data: data, @@ -529,4 +474,15 @@ class LoginHttp { }; } } + + static Future 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']}; + } } diff --git a/lib/http/member.dart b/lib/http/member.dart index 004dc3a58..9b9fd9dc3 100644 --- a/lib/http/member.dart +++ b/lib/http/member.dart @@ -65,11 +65,7 @@ class MemberHttp { required int mid, required int page, }) async { - String? accessKey = GStorage.localCache - .get(LocalCacheKey.accessKey, defaultValue: {})['value']; Map 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 spaceFav({ required int mid, }) async { - String? accessKey = GStorage.localCache - .get(LocalCacheKey.accessKey, defaultValue: {})['value']; Map 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 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 space({ int? mid, }) async { - String? accessKey = GStorage.localCache - .get(LocalCacheKey.accessKey, defaultValue: {})['value']; Map 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, diff --git a/lib/http/reply.dart b/lib/http/reply.dart index 989bdf59a..92ed1e3a6 100644 --- a/lib/http/reply.dart +++ b/lib/http/reply.dart @@ -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); diff --git a/lib/http/video.dart b/lib/http/video.dart index 63567b858..9dd755f1a 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -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 rcmdVideoListApp( - {bool loginStatus = true, required int freshIdx}) async { + static Future rcmdVideoListApp({required int freshIdx}) async { Map 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, diff --git a/lib/models/common/rcmd_type.dart b/lib/models/common/rcmd_type.dart deleted file mode 100644 index 2dfdad1ce..000000000 --- a/lib/models/common/rcmd_type.dart +++ /dev/null @@ -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]; -} diff --git a/lib/pages/about/index.dart b/lib/pages/about/index.dart index c6a7b385c..90c54d48c 100644 --- a/lib/pages/about/index.dart +++ b/lib/pages/about/index.dart @@ -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('重置成功'); }, diff --git a/lib/pages/common/reply_controller.dart b/lib/pages/common/reply_controller.dart index 2ae3cd487..a2c8a341b 100644 --- a/lib/pages/common/reply_controller.dart +++ b/lib/pages/common/reply_controller.dart @@ -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.MAIN_LIST_HOT.obs; @@ -374,10 +373,10 @@ abstract class ReplyController extends CommonController { // biliSendCommAntifraud if (Platform.isAndroid && _biliSendCommAntifraud) { try { - List 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', diff --git a/lib/pages/dynamics/widgets/author_panel.dart b/lib/pages/dynamics/widgets/author_panel.dart index 4334dbccb..707ce20d0 100644 --- a/lib/pages/dynamics/widgets/author_panel.dart +++ b/lib/pages/dynamics/widgets/author_panel.dart @@ -312,7 +312,7 @@ class AuthorPanel extends StatelessWidget { }, minLeadingWidth: 0, ), - if (GStorage.isLogin) + if (Accounts.main.isLogin) ListTile( title: Text( '举报', diff --git a/lib/pages/live/controller.dart b/lib/pages/live/controller.dart index 22321411b..ba9e12352 100644 --- a/lib/pages/live/controller.dart +++ b/lib/pages/live/controller.dart @@ -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 followListState = LoadingState.loading().obs; late int followPage = 1; late bool followEnd = false; diff --git a/lib/pages/login/controller.dart b/lib/pages/login/controller.dart index 327bcea6d..5dba65654 100644 --- a/lib/pages/login/controller.dart +++ b/lib/pages/login/controller.dart @@ -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 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( + 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('确定'), + ), + ], + ); + }), + ); + } } diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart index 40822eeec..df7820102 100644 --- a/lib/pages/main/controller.dart +++ b/lib/pages/main/controller.dart @@ -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)]; diff --git a/lib/pages/member/new/widget/edit_profile_page.dart b/lib/pages/member/new/widget/edit_profile_page.dart index 7b65886d1..3854cb244 100644 --- a/lib/pages/member/new/widget/edit_profile_page.dart +++ b/lib/pages/member/new/widget/edit_profile_page.dart @@ -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 { _getInfo() async { Map 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 { '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 { dynamic datum, }) async { Map 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 { 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', diff --git a/lib/pages/member_coin/view.dart b/lib/pages/member_coin/view.dart index 615eaa994..c047a4258 100644 --- a/lib/pages/member_coin/view.dart +++ b/lib/pages/member_coin/view.dart @@ -24,7 +24,7 @@ class MemberCoinPage extends StatefulWidget { } class _MemberCoinPageState extends State { - late final _ownerMid = GStorage.ownerMid; + late final _ownerMid = Accounts.main.mid; late final _ctr = Get.put( MemberCoinController(mid: widget.mid), diff --git a/lib/pages/member_like/view.dart b/lib/pages/member_like/view.dart index 49d177440..c4bea1d02 100644 --- a/lib/pages/member_like/view.dart +++ b/lib/pages/member_like/view.dart @@ -24,7 +24,7 @@ class MemberLikePage extends StatefulWidget { } class _MemberLikePageState extends State { - late final _ownerMid = GStorage.ownerMid; + late final _ownerMid = Accounts.main.mid; late final _ctr = Get.put( MemberLikeController(mid: widget.mid), diff --git a/lib/pages/mine/controller.dart b/lib/pages/mine/controller.dart index 5b6307f61..45507159b 100644 --- a/lib/pages/mine/controller.dart +++ b/lib/pages/mine/controller.dart @@ -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.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, diff --git a/lib/pages/mine/view.dart b/lib/pages/mine/view.dart index 01f4af177..d1d831539 100644 --- a/lib/pages/mine/view.dart +++ b/lib/pages/mine/view.dart @@ -129,6 +129,7 @@ class _MinePageState extends State { GestureDetector( behavior: HitTestBehavior.opaque, onTap: _mineController.onLogin, + onLongPress: () => _mineController.onLogin(true), child: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/pages/rcmd/controller.dart b/lib/pages/rcmd/controller.dart index 9fe1d6aa3..6a1d8be5f 100644 --- a/lib/pages/rcmd/controller.dart +++ b/lib/pages/rcmd/controller.dart @@ -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 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 diff --git a/lib/pages/setting/recommend_setting.dart b/lib/pages/setting/recommend_setting.dart index 4c1bfebfa..54447a209 100644 --- a/lib/pages/setting/recommend_setting.dart +++ b/lib/pages/setting/recommend_setting.dart @@ -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' '* 后续可能会增加更多过滤条件,敬请期待。', diff --git a/lib/pages/setting/sponsor_block_page.dart b/lib/pages/setting/sponsor_block_page.dart index 9c3c27d1f..522dba759 100644 --- a/lib/pages/setting/sponsor_block_page.dart +++ b/lib/pages/setting/sponsor_block_page.dart @@ -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 { Request() .get( '$_blockServer/api/status/uptime', - options: Options(extra: {'clearCookie': true}), ) .then((res) { setState(() { diff --git a/lib/pages/setting/view.dart b/lib/pages/setting/view.dart index fdfe5ad20..bca1f8b8d 100644 --- a/lib/pages/setting/view.dart +++ b/lib/pages/setting/view.dart @@ -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 { 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 { : 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 { ), ), ), - 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('确认'), diff --git a/lib/pages/setting/widgets/model.dart b/lib/pages/setting/widgets/model.dart index 1713a7f5e..58e4f978d 100644 --- a/lib/pages/setting/widgets/model.dart +++ b/lib/pages/setting/widgets/model.dart @@ -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 get videoSettings => [ List 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( - 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().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 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 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 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 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 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 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 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 get extraSettings => [ title: '屏蔽带货评论', leading: Stack( alignment: Alignment.center, - children: [ + children: const [ Icon(Icons.shopping_bag_outlined, size: 14), Icon(Icons.not_interested), ], diff --git a/lib/pages/setting/widgets/select_dialog.dart b/lib/pages/setting/widgets/select_dialog.dart index 995079a4e..111f6ec3f 100644 --- a/lib/pages/setting/widgets/select_dialog.dart +++ b/lib/pages/setting/widgets/select_dialog.dart @@ -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 extends State> { 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; diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index cc1ba7d04..a68234b0a 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -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) { diff --git a/lib/pages/video/detail/post_panel/post_panel.dart b/lib/pages/video/detail/post_panel/post_panel.dart index d15320cd0..14b74e12f 100644 --- a/lib/pages/video/detail/post_panel/post_panel.dart +++ b/lib/pages/video/detail/post_panel/post_panel.dart @@ -330,8 +330,7 @@ class _PostPanelState extends CommonCollapseSlidePageState { TextButton( onPressed: () { Get.back(); - Request() - .post( + Request().post( '${GStorage.blockServer}/api/skipSegments', queryParameters: { 'videoID': videoDetailController.bvid, @@ -355,9 +354,7 @@ class _PostPanelState extends CommonCollapseSlidePageState { ) .toList(), }, - options: videoDetailController.options, - ) - .then( + ).then( (res) { if (res.statusCode == 200) { Get.back(); diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index d51f68eee..630a0aa3a 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -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, diff --git a/lib/pages/video/detail/reply/widgets/reply_item_grpc.dart b/lib/pages/video/detail/reply/widgets/reply_item_grpc.dart index d1dc75420..e16a192f3 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item_grpc.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item_grpc.dart @@ -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, diff --git a/lib/pages/webview/webview_page.dart b/lib/pages/webview/webview_page.dart index 223f838c7..8a1a9320b 100644 --- a/lib/pages/webview/webview_page.dart +++ b/lib/pages/webview/webview_page.dart @@ -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 { } break; case _WebviewMenuItem.resetCookie: - final List 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, diff --git a/lib/utils/accounts/account.dart b/lib/utils/accounts/account.dart new file mode 100644 index 000000000..5445c8380 --- /dev/null +++ b/lib/utils/accounts/account.dart @@ -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 type; + + final int mid = 0; + late String csrf; + final Map headers = const {}; + + bool activited = false; + + Future logout(); + Future onChange(); + + Map? 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 type; + + @override + late final int mid = int.parse(_midStr); + + @override + late final Map 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 logout() async { + await Future.wait([cookieJar.deleteAll(), _box.delete(_midStr)]); + return AnonymousAccount(); + } + + @override + Future onChange() => _box.put(_midStr, this); + + @override + Map? 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 _box = Accounts.account; + + LoginAccount(this.cookieJar, this.accessKey, this.refresh, + [Set? 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 type = {}; + @override + final int mid = 0; + @override + String csrf = ''; + @override + final Map headers = const {}; + + @override + bool activited = false; + + @override + Future logout() async { + await cookieJar.deleteAll(); + activited = false; + return this; + } + + @override + Future onChange() async {} + + @override + Map? 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 toJson() { + final cookies = domainCookies['bilibili.com']?['/'] ?? {}; + return {for (var i in cookies.values) i.cookie.name: i.cookie.value}; + } + + List 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()), + }, + }; +} diff --git a/lib/utils/accounts/account_adapter.dart b/lib/utils/accounts/account_adapter.dart new file mode 100644 index 000000000..2cd28fc79 --- /dev/null +++ b/lib/utils/accounts/account_adapter.dart @@ -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 { + @override + final int typeId = 9; + + @override + LoginAccount read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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().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; +} diff --git a/lib/utils/accounts/account_manager/LICENSE b/lib/utils/accounts/account_manager/LICENSE new file mode 100644 index 000000000..cef745feb --- /dev/null +++ b/lib/utils/accounts/account_manager/LICENSE @@ -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. \ No newline at end of file diff --git a/lib/utils/accounts/account_manager/README.md b/lib/utils/accounts/account_manager/README.md new file mode 100644 index 000000000..4358009f2 --- /dev/null +++ b/lib/utils/accounts/account_manager/README.md @@ -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 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)!, +); +``` diff --git a/lib/utils/accounts/account_manager/account_mgr.dart b/lib/utils/accounts/account_manager/account_mgr.dart new file mode 100644 index 000000000..cf76ae595 --- /dev/null +++ b/lib/utils/accounts/account_manager/account_mgr.dart @@ -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> 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 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(); + 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 _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 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 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]; +} diff --git a/lib/utils/accounts/account_type_adapter.dart b/lib/utils/accounts/account_type_adapter.dart new file mode 100644 index 000000000..94a59be81 --- /dev/null +++ b/lib/utils/accounts/account_type_adapter.dart @@ -0,0 +1,28 @@ +import 'package:PiliPlus/utils/extension.dart'; +import 'package:hive/hive.dart'; + +import '../storage.dart' show AccountType; + +class AccountTypeAdapter extends TypeAdapter { + @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; +} diff --git a/lib/utils/accounts/cookie_jar_adapter.dart b/lib/utils/accounts/cookie_jar_adapter.dart new file mode 100644 index 000000000..14f83a67a --- /dev/null +++ b/lib/utils/accounts/cookie_jar_adapter.dart @@ -0,0 +1,28 @@ +import 'package:cookie_jar/cookie_jar.dart'; +import 'package:hive/hive.dart'; + +import 'account.dart'; + +class BiliCookieJarAdapter extends TypeAdapter { + @override + final int typeId = 8; + + @override + DefaultCookieJar read(BinaryReader reader) => + BiliCookieJar.fromJson(reader.readMap().cast()); + + @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; +} diff --git a/lib/utils/data.dart b/lib/utils/data.dart index 633505f08..002bea9e5 100644 --- a/lib/utils/data.dart +++ b/lib/utils/data.dart @@ -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(); diff --git a/lib/utils/login.dart b/lib/utils/login.dart index 088d633c9..9fd8b30f9 100644 --- a/lib/utils/login.dart +++ b/lib/utils/login.dart @@ -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 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 cookieInfo = jsonCookieInfo['cookies']; - List 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 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() ..isLogin.value = true @@ -85,14 +62,14 @@ class LoginUtils { try { Get.find() ..isLogin.value = true - ..userFace.value = result['data'].face; + ..userFace.value = data.face!; } catch (_) {} try { Get.find() ..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() - ..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().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 { diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 1a9e9ee8d..ae925a791 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -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 setting; static late final Box 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 get speedList => List.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 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 account; + static final Map accountMode = {}; + static Account get main => accountMode[AccountType.main]!; + // static set main(Account account) => set(AccountType.main, account); + + static Future init() async { + account = await Hive.openBox('account', + compactionStrategy: (int entries, int deletedEntries) { + return deletedEntries > 2; + }); + await _migrate(); + } + + static Future _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 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 clear() async { + await account.clear(); + for (var i in AccountType.values) { + accountMode[i] = AnonymousAccount(); + } + if (!AnonymousAccount().activited) { + Request.buvidActive(AnonymousAccount()); + } + } + + static Future close() async { + account.compact(); + account.close(); + } + + static Future 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]; +} diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 757d3ca08..c87864fc2 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -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 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 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 params, String appkey, String appsec) { + static void appSign(Map 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 generateRandomBytes(int minLength, int maxLength) { diff --git a/pubspec.lock b/pubspec.lock index 32fbb64b7..5b39ae852 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -425,14 +425,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.7.0" - dio_cookie_manager: - dependency: "direct main" - description: - name: dio_cookie_manager - sha256: e79498b0f632897ff0c28d6e8178b4bc6e9087412401f618c31fa0904ace050d - url: "https://pub.dev" - source: hosted - version: "3.1.1" dio_http2_adapter: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 9359b2b97..569fdda06 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,7 +45,6 @@ dependencies: # 网络 dio: ^5.7.0 cookie_jar: ^4.0.8 - dio_cookie_manager: ^3.1.1 connectivity_plus: ^6.1.1 dio_http2_adapter: ^2.5.3