feat: 新版登录页:以APP接口和新界面全面重构网页版登录;更新二维码与极验插件;更新版本号

This commit is contained in:
orz12
2024-07-07 15:31:58 +08:00
parent 8bb990015c
commit 8daf603fdb
17 changed files with 1575 additions and 1114 deletions

View File

@@ -17,4 +17,223 @@ class Constants {
static const String thirdSign = '04224646d1fea004e79606d3b038c84a';
static const String thirdApi =
'https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png';
//内容来自 https://passport.bilibili.com/web/generic/country/list
static const List<Map<String, dynamic>> internationalDialingPrefix = [
{"id": 1, "cname": "中国大陆", "country_id": "86"},
{"id": 5, "cname": "中国香港特别行政区", "country_id": "852"},
{"id": 2, "cname": "中国澳门特别行政区", "country_id": "853"},
{"id": 3, "cname": "中国台湾", "country_id": "886"},
{"id": 4, "cname": "美国", "country_id": "1"},
{"id": 6, "cname": "比利时", "country_id": "32"},
{"id": 7, "cname": "澳大利亚", "country_id": "61"},
{"id": 8, "cname": "法国", "country_id": "33"},
{"id": 9, "cname": "加拿大", "country_id": "1"},
{"id": 10, "cname": "日本", "country_id": "81"},
{"id": 11, "cname": "新加坡", "country_id": "65"},
{"id": 12, "cname": "韩国", "country_id": "82"},
{"id": 13, "cname": "马来西亚", "country_id": "60"},
{"id": 14, "cname": "英国", "country_id": "44"},
{"id": 15, "cname": "意大利", "country_id": "39"},
{"id": 16, "cname": "德国", "country_id": "49"},
{"id": 18, "cname": "俄罗斯", "country_id": "7"},
{"id": 19, "cname": "新西兰", "country_id": "64"}, //common:1-19
{"id": 153, "cname": "瓦利斯群岛和富图纳群岛", "country_id": "1681"},
{"id": 152, "cname": "葡萄牙", "country_id": "351"},
{"id": 151, "cname": "帕劳", "country_id": "680"},
{"id": 150, "cname": "诺福克岛", "country_id": "672"},
{"id": 149, "cname": "挪威", "country_id": "47"},
{"id": 148, "cname": "纽埃岛", "country_id": "683"},
{"id": 147, "cname": "尼日利亚", "country_id": "234"},
{"id": 146, "cname": "尼日尔", "country_id": "227"},
{"id": 145, "cname": "尼加拉瓜", "country_id": "505"},
{"id": 144, "cname": "尼泊尔", "country_id": "977"},
{"id": 143, "cname": "瑙鲁", "country_id": "674"},
{"id": 154, "cname": "格鲁吉亚", "country_id": "995"},
{"id": 155, "cname": "瑞典", "country_id": "46"},
{"id": 165, "cname": "沙特阿拉伯", "country_id": "966"},
{"id": 164, "cname": "桑给巴尔岛", "country_id": "259"},
{"id": 163, "cname": "塞舌尔共和国", "country_id": "248"},
{"id": 162, "cname": "塞浦路斯", "country_id": "357"},
{"id": 161, "cname": "塞内加尔", "country_id": "221"},
{"id": 160, "cname": "塞拉利昂", "country_id": "232"},
{"id": 159, "cname": "萨摩亚,东部", "country_id": "684"},
{"id": 158, "cname": "萨摩亚,西部", "country_id": "685"},
{"id": 157, "cname": "萨尔瓦多", "country_id": "503"},
{"id": 156, "cname": "瑞士", "country_id": "41"},
{"id": 166, "cname": "圣多美和普林西比", "country_id": "239"},
{"id": 142, "cname": "塞尔维亚", "country_id": "381"},
{"id": 141, "cname": "南非", "country_id": "27"},
{"id": 128, "cname": "毛里塔尼亚", "country_id": "222"},
{"id": 127, "cname": "毛里求斯", "country_id": "230"},
{"id": 126, "cname": "马歇尔岛", "country_id": "692"},
{"id": 125, "cname": "马提尼克岛", "country_id": "596"},
{"id": 124, "cname": "马其顿", "country_id": "389"},
{"id": 123, "cname": "马里亚纳岛", "country_id": "1670"},
{"id": 122, "cname": "马里", "country_id": "223"},
{"id": 121, "cname": "马拉维", "country_id": "265"},
{"id": 120, "cname": "马耳他", "country_id": "356"},
{"id": 119, "cname": "马尔代夫", "country_id": "960"},
{"id": 129, "cname": "蒙古", "country_id": "976"},
{"id": 130, "cname": "蒙特塞拉特岛", "country_id": "1664"},
{"id": 140, "cname": "纳米比亚", "country_id": "264"},
{"id": 139, "cname": "墨西哥", "country_id": "52"},
{"id": 138, "cname": "莫桑比克", "country_id": "258"},
{"id": 137, "cname": "摩纳哥", "country_id": "377"},
{"id": 136, "cname": "摩洛哥", "country_id": "212"},
{"id": 135, "cname": "摩尔多瓦", "country_id": "373"},
{"id": 134, "cname": "缅甸", "country_id": "95"},
{"id": 133, "cname": "密克罗尼西亚", "country_id": "691"},
{"id": 132, "cname": "秘鲁", "country_id": "51"},
{"id": 131, "cname": "孟加拉国", "country_id": "880"},
{"id": 118, "cname": "马达加斯加", "country_id": "261"},
{"id": 167, "cname": "圣卢西亚", "country_id": "1784"},
{"id": 216, "cname": "智利", "country_id": "56"},
{"id": 203, "cname": "牙买加", "country_id": "1876"},
{"id": 202, "cname": "叙利亚", "country_id": "963"},
{"id": 201, "cname": "匈牙利", "country_id": "36"},
{"id": 200, "cname": "科特迪瓦", "country_id": "225"},
{"id": 199, "cname": "希腊", "country_id": "30"},
{"id": 198, "cname": "西班牙", "country_id": "34"},
{"id": 197, "cname": "乌兹别克斯坦", "country_id": "998"},
{"id": 196, "cname": "乌拉圭", "country_id": "598"},
{"id": 195, "cname": "乌克兰", "country_id": "380"},
{"id": 194, "cname": "乌干达", "country_id": "256"},
{"id": 204, "cname": "亚美尼亚", "country_id": "374"},
{"id": 205, "cname": "也门", "country_id": "967"},
{"id": 215, "cname": "直布罗陀", "country_id": "350"},
{"id": 214, "cname": "乍得", "country_id": "235"},
{"id": 213, "cname": "赞比亚", "country_id": "260"},
{"id": 212, "cname": "越南", "country_id": "84"},
{"id": 211, "cname": "约旦", "country_id": "962"},
{"id": 210, "cname": "印尼", "country_id": "62"},
{"id": 209, "cname": "印度", "country_id": "91"},
{"id": 208, "cname": "以色列", "country_id": "972"},
{"id": 207, "cname": "伊朗", "country_id": "98"},
{"id": 206, "cname": "伊拉克", "country_id": "964"},
{"id": 193, "cname": "文莱", "country_id": "673"},
{"id": 192, "cname": "委内瑞拉", "country_id": "58"},
{"id": 191, "cname": "维珍群岛(英属)", "country_id": "1284"},
{"id": 178, "cname": "泰国", "country_id": "66"},
{"id": 177, "cname": "索马里", "country_id": "252"},
{"id": 176, "cname": "所罗门群岛", "country_id": "677"},
{"id": 175, "cname": "苏里南", "country_id": "597"},
{"id": 174, "cname": "苏丹", "country_id": "249"},
{"id": 173, "cname": "斯威士兰", "country_id": "268"},
{"id": 172, "cname": "斯洛文尼亚", "country_id": "386"},
{"id": 171, "cname": "斯洛伐克", "country_id": "421"},
{"id": 170, "cname": "斯里兰卡", "country_id": "94"},
{"id": 169, "cname": "圣皮埃尔和密克隆群岛", "country_id": "508"},
{"id": 179, "cname": "坦桑尼亚", "country_id": "255"},
{"id": 180, "cname": "汤加", "country_id": "676"},
{"id": 190, "cname": "维珍群岛(美属)", "country_id": "1340"},
{"id": 189, "cname": "瓦努阿图", "country_id": "678"},
{"id": 188, "cname": "托克劳岛", "country_id": "690"},
{"id": 187, "cname": "土库曼斯坦", "country_id": "993"},
{"id": 186, "cname": "土耳其", "country_id": "90"},
{"id": 185, "cname": "图瓦卢", "country_id": "688"},
{"id": 184, "cname": "突尼斯", "country_id": "216"},
{"id": 183, "cname": "阿森松岛", "country_id": "247"},
{"id": 182, "cname": "特立尼达和多巴哥", "country_id": "1868"},
{"id": 181, "cname": "特克斯和凯科斯", "country_id": "1649"},
{"id": 168, "cname": "圣马力诺", "country_id": "378"},
{"id": 67, "cname": "法属圭亚那", "country_id": "594"},
{"id": 54, "cname": "不丹", "country_id": "975"},
{"id": 53, "cname": "博茨瓦纳", "country_id": "267"},
{"id": 52, "cname": "伯利兹", "country_id": "501"},
{"id": 51, "cname": "玻利维亚", "country_id": "591"},
{"id": 50, "cname": "波兰", "country_id": "48"},
{"id": 49, "cname": "波黑", "country_id": "387"},
{"id": 48, "cname": "波多黎各", "country_id": "1787"},
{"id": 47, "cname": "冰岛", "country_id": "354"},
{"id": 46, "cname": "贝宁", "country_id": "229"},
{"id": 45, "cname": "保加利亚", "country_id": "359"},
{"id": 55, "cname": "布基纳法索", "country_id": "226"},
{"id": 56, "cname": "布隆迪", "country_id": "257"},
{"id": 66, "cname": "法属波利尼西亚", "country_id": "689"},
{"id": 65, "cname": "法罗岛", "country_id": "298"},
{"id": 64, "cname": "厄立特里亚", "country_id": "291"},
{"id": 63, "cname": "厄瓜多尔", "country_id": "593"},
{"id": 62, "cname": "多米尼加代表", "country_id": "1809"},
{"id": 61, "cname": "多米尼加", "country_id": "1767"},
{"id": 60, "cname": "多哥", "country_id": "228"},
{"id": 59, "cname": "迪戈加西亚岛", "country_id": "246"},
{"id": 58, "cname": "丹麦", "country_id": "45"},
{"id": 57, "cname": "赤道几内亚", "country_id": "240"},
{"id": 44, "cname": "百慕大群岛", "country_id": "1441"},
{"id": 43, "cname": "白俄罗斯", "country_id": "375"},
{"id": 42, "cname": "巴西", "country_id": "55"},
{"id": 29, "cname": "爱尔兰", "country_id": "353"},
{"id": 28, "cname": "埃塞俄比亚", "country_id": "251"},
{"id": 27, "cname": "埃及", "country_id": "20"},
{"id": 26, "cname": "阿塞拜疆", "country_id": "994"},
{"id": 25, "cname": "阿曼", "country_id": "968"},
{"id": 24, "cname": "阿联酋", "country_id": "971"},
{"id": 23, "cname": "阿根廷", "country_id": "54"},
{"id": 22, "cname": "阿富汗", "country_id": "93"},
{"id": 21, "cname": "阿尔及利亚", "country_id": "213"},
{"id": 20, "cname": "阿尔巴尼亚", "country_id": "355"},
{"id": 30, "cname": "爱沙尼亚", "country_id": "372"},
{"id": 31, "cname": "安道尔", "country_id": "376"},
{"id": 41, "cname": "巴拿马", "country_id": "507"},
{"id": 40, "cname": "巴林", "country_id": "973"},
{"id": 39, "cname": "巴拉圭", "country_id": "595"},
{"id": 38, "cname": "巴基斯坦", "country_id": "92"},
{"id": 37, "cname": "巴哈马群岛", "country_id": "1242"},
{"id": 36, "cname": "巴布亚新几内亚", "country_id": "675"},
{"id": 35, "cname": "巴巴多斯", "country_id": "1246"},
{"id": 34, "cname": "奥地利", "country_id": "43"},
{"id": 33, "cname": "安提瓜岛和巴布达", "country_id": "1268"},
{"id": 32, "cname": "安哥拉", "country_id": "244"},
{"id": 68, "cname": "非洲中部", "country_id": "236"},
{"id": 117, "cname": "罗马尼亚", "country_id": "40"},
{"id": 104, "cname": "科威特", "country_id": "965"},
{"id": 103, "cname": "科摩罗", "country_id": "269"},
{"id": 102, "cname": "开曼群岛", "country_id": "1345"},
{"id": 101, "cname": "卡塔尔", "country_id": "974"},
{"id": 100, "cname": "喀麦隆", "country_id": "237"},
{"id": 99, "cname": "聚会岛", "country_id": "262"},
{"id": 98, "cname": "津巴布韦", "country_id": "263"},
{"id": 97, "cname": "捷克", "country_id": "420"},
{"id": 96, "cname": "柬埔寨", "country_id": "855"},
{"id": 95, "cname": "加蓬", "country_id": "241"},
{"id": 105, "cname": "克罗地亚", "country_id": "385"},
{"id": 106, "cname": "肯尼亚", "country_id": "254"},
{"id": 116, "cname": "卢旺达", "country_id": "250"},
{"id": 115, "cname": "卢森堡", "country_id": "352"},
{"id": 114, "cname": "利比亚", "country_id": "218"},
{"id": 113, "cname": "利比里亚", "country_id": "231"},
{"id": 112, "cname": "立陶宛", "country_id": "370"},
{"id": 111, "cname": "黎巴嫩", "country_id": "961"},
{"id": 110, "cname": "老挝", "country_id": "856"},
{"id": 109, "cname": "莱索托", "country_id": "266"},
{"id": 108, "cname": "拉脱维亚", "country_id": "371"},
{"id": 107, "cname": "库克岛", "country_id": "682"},
{"id": 94, "cname": "加纳", "country_id": "233"},
{"id": 93, "cname": "几内亚比绍", "country_id": "245"},
{"id": 92, "cname": "几内亚", "country_id": "224"},
{"id": 79, "cname": "格林纳达", "country_id": "1473"},
{"id": 78, "cname": "哥斯达黎加", "country_id": "506"},
{"id": 77, "cname": "哥伦比亚", "country_id": "57"},
{"id": 76, "cname": "刚果(金)", "country_id": "243"},
{"id": 75, "cname": "刚果", "country_id": "242"},
{"id": 74, "cname": "冈比亚", "country_id": "220"},
{"id": 73, "cname": "福克兰岛", "country_id": "500"},
{"id": 72, "cname": "佛得角", "country_id": "238"},
{"id": 71, "cname": "芬兰", "country_id": "358"},
{"id": 70, "cname": "斐济", "country_id": "679"},
{"id": 80, "cname": "格陵兰岛", "country_id": "299"},
{"id": 81, "cname": "古巴", "country_id": "53"},
{"id": 91, "cname": "吉尔吉斯斯坦", "country_id": "996"},
{"id": 90, "cname": "吉布提", "country_id": "253"},
{"id": 89, "cname": "基里巴斯", "country_id": "686"},
{"id": 88, "cname": "维克岛", "country_id": "1808"},
{"id": 87, "cname": "洪都拉斯", "country_id": "504"},
{"id": 86, "cname": "荷兰", "country_id": "31"},
{"id": 85, "cname": "朝鲜", "country_id": "850"},
{"id": 84, "cname": "海地", "country_id": "509"},
{"id": 83, "cname": "关岛", "country_id": "1671"},
{"id": 82, "cname": "瓜德罗普岛", "country_id": "590"},
{"id": 69, "cname": "菲律宾", "country_id": "63"}
];
}

View File

@@ -77,7 +77,7 @@ class VideoPopupMenu extends StatelessWidget {
String? accessKey = GStorage.localCache
.get(LocalCacheKey.accessKey, defaultValue: {})['value'];
if (accessKey == null || accessKey == "") {
SmartDialog.showToast("本操作使用app端接口请前往【隐私设置】刷新access_key");
SmartDialog.showToast("请退出账号后重新登录");
return;
}
if (videoItem is RecVideoItemAppModel) {

View File

@@ -462,12 +462,19 @@ class Api {
// web端验证码登录
// web端密码登录
static const String logInByWebPwd =
'${HttpString.passBaseUrl}/x/passport-login/web/login';
// 获取guestID
// static const String getGuestId = '/x/passport-user/guest/reg';
// app端短信验证码
static const String appSmsCode =
'${HttpString.passBaseUrl}/x/passport-login/sms/send';
// app端验证码登录
static const String logInByAppSms =
'${HttpString.passBaseUrl}/x/passport-login/login/sms';
// 获取短信验证码
// static const String appSafeSmsCode =
@@ -477,8 +484,8 @@ class Api {
/// username
/// password
/// key
/// rhash
static const String loginInByPwdApi =
/// salt
static const String loginByPwdApi =
'${HttpString.passBaseUrl}/x/passport-login/oauth2/login';
/// 密码加密密钥

View File

@@ -3,43 +3,42 @@
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart';
import '../utils/storage.dart';
class ApiInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// print("请求之前");
// 在请求之前添加头部或认证信息
// options.headers['Authorization'] = 'Bearer token';
// options.headers['Content-Type'] = 'application/json';
handler.next(options);
}
// @override
// void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// print("请求之前");
// // 在请求之前添加头部或认证信息
// options.headers['Authorization'] = 'Bearer token';
// options.headers['Content-Type'] = 'application/json';
// handler.next(options);
// }
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
try {
if (response.statusCode == 302) {
final List<String> locations = response.headers['location']!;
if (locations.isNotEmpty) {
if (locations.first.startsWith('https://www.mcbbs.net')) {
final Uri uri = Uri.parse(locations.first);
final String? accessKey = uri.queryParameters['access_key'];
final String? mid = uri.queryParameters['mid'];
try {
Box localCache = GStorage.localCache;
localCache.put(LocalCacheKey.accessKey,
<String, String?>{'mid': mid, 'value': accessKey});
} catch (_) {}
}
}
}
} catch (err) {
print('ApiInterceptor: $err');
}
// @override
// void onResponse(Response response, ResponseInterceptorHandler handler) {
// try {
// if (response.statusCode == 302) {
// final List<String> locations = response.headers['location']!;
// if (locations.isNotEmpty) {
// if (locations.first.startsWith('https://www.mcbbs.net')) {
// print('ApiInterceptor@@@@@: ${locations.first}');
// final Uri uri = Uri.parse(locations.first);
// final String? accessKey = uri.queryParameters['access_key'];
// final String? mid = uri.queryParameters['mid'];
// try {
// Box localCache = GStorage.localCache;
// localCache.put(LocalCacheKey.accessKey,
// <String, String?>{'mid': mid, 'value': accessKey});
// } catch (_) {}
// }
// }
// }
// } catch (err) {
// print('ApiInterceptor: $err');
// }
handler.next(response);
}
// handler.next(response);
// }
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {

View File

@@ -3,12 +3,68 @@ import 'dart:math';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
import 'package:encrypt/encrypt.dart';
import 'package:uuid/uuid.dart';
import '../common/constants.dart';
import '../models/login/index.dart';
import '../utils/login.dart';
import '../utils/utils.dart';
import 'index.dart';
class LoginHttp {
static String deviceId = genDeviceId();
static String buvid = genBuvid();
static String host = 'passport.bilibili.com';
static String traceId =
'11111111111111111111111111111111:1111111111111111:0:0';
static String statistics = Uri.encodeComponent(
'{"appId": 5,"platform": 3,"version": "1.46.2","abtest": ""}');
static String userAgent =
'Mozilla/5.0 BiliDroid/1.46.2 (bbcallen@gmail.com) os/android model/vivo mobi_app/android_hd build/1462100 channel/yingyongbao innerVer/1462100 osVer/14 network/2';
static Future<Map<String, dynamic>> getHDcode() async {
var params = {
'appkey': Constants.appKey,
// 'local_id': 'Y952A395BB157D305D8A8340FC2AAECECE17',
'local_id': '0',
//精确到秒的时间戳
'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(),
'platform': 'android',
'mobi_app': 'android_hd',
};
String sign = Utils.appSign(
params,
Constants.appKey,
Constants.appSec,
);
var res = await Request()
.post(Api.getTVCode, queryParameters: {...params, 'sign': sign});
print(res);
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
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});
return {
'status': res.data['code'] == 0,
'code': res.data['code'],
'data': res.data['data'],
'msg': res.data['message']
};
}
static Future queryCaptcha() async {
var res = await Request().get(Api.getCaptcha);
if (res.data['code'] == 0) {
@@ -21,107 +77,141 @@ class LoginHttp {
}
}
// 获取salt与PubKey
static Future getWebKey() async {
var res = await Request().get(Api.getWebKey);
//data: {'disable_rcmd': 0, 'local_id': LoginUtils.generateBuvid()});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': {}, 'msg': res.data['message']};
}
}
static Future sendSmsCode({
int? cid,
required int tel,
required String token,
required String challenge,
required String validate,
required String seccode,
required String cid,
required String tel,
// String? deviceTouristId,
String? gee_challenge,
String? gee_seccode,
String? gee_validate,
String? recaptcha_token,
}) async {
int timestamp = DateTime.now().millisecondsSinceEpoch;
var data = {
'appkey': Constants.appKey,
'build': '1462100',
'buvid': buvid,
'c_locale': 'zh_CN',
'cid': cid,
// if (deviceTouristId != null) 'device_tourist_id': deviceTouristId,
'disable_rcmd': '0',
if (gee_challenge != null) 'gee_challenge': gee_challenge,
if (gee_seccode != null) 'gee_seccode': gee_seccode,
if (gee_validate != null) 'gee_validate': gee_validate,
'local_id': buvid,
// https://chinggg.github.io/post/appre/
'login_session_id':
md5.convert(utf8.encode(buvid + timestamp.toString())).toString(),
'mobi_app': 'android_hd',
'platform': 'android',
if (recaptcha_token != null) 'recaptcha_token': recaptcha_token,
's_locale': 'zh_CN',
'statistics': statistics,
'tel': tel,
'ts': (timestamp ~/ 1000).toString(),
};
String sign = Utils.appSign(
data,
Constants.appKey,
Constants.appSec,
);
var headers = {
'Host': host,
'buvid': buvid,
'env': 'prod',
'app-key': 'android_hd',
'user-agent': userAgent,
'x-bili-trace-id': traceId,
'x-bili-aurora-eid': '',
'x-bili-aurora-zone': '',
'bili-http-engine': 'cronet',
'content-type': 'application/x-www-form-urlencoded; charset=utf-8',
};
var res = await Request().post(
Api.appSmsCode,
data: {
'cid': cid,
'tel': tel,
"source": "main_web",
'token': token,
'challenge': challenge,
'validate': validate,
'seccode': seccode,
},
data: {...data, 'sign': sign},
options: Options(
contentType: Headers.formUrlEncodedContentType,
// headers: {'user-agent': ApiConstants.userAgent}
headers: headers,
),
);
print(res);
if (res.data['code'] == 0 && res.data['data']['recaptcha_url'] == "") {
return {'status': true, 'data': res.data['data']};
} else {
return {
'status': false,
'code': res.data['code'],
'msg': res.data['message'],
'data': res.data['data']
};
}
}
// web端验证码
static Future sendWebSmsCode({
int? cid,
required int tel,
required String token,
required String challenge,
required String validate,
required String seccode,
}) async {
Map data = {
'cid': cid,
'tel': tel,
'token': token,
'challenge': challenge,
'validate': validate,
'seccode': seccode,
};
FormData formData = FormData.fromMap({...data});
var res = await Request().post(
Api.smsCode,
data: formData,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
);
print(res);
}
// static Future getGuestId(String key) async {
// dynamic publicKey = RSAKeyParser().parse(key);
// var params = {
// 'appkey': Constants.appKey,
// 'build': '1462100',
// 'buvid': buvid,
// 'c_locale': 'zh_CN',
// 'channel': 'yingyongbao',
// 'deviceInfo': 'xxxxxx',
// 'disable_rcmd': '0',
// 'dt': Uri.encodeComponent(Encrypter(RSA(publicKey: publicKey))
// .encrypt(generateRandomString(16))
// .base64),
// 'local_id': buvid,
// 'mobi_app': 'android_hd',
// 'platform': 'android',
// 's_locale': 'zh_CN',
// 'statistics': statistics,
// 'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(),
// };
// String sign = Utils.appSign(
// params,
// Constants.appKey,
// Constants.appSec,
// );
// var headers = {
// 'Host': host,
// 'buvid': buvid,
// 'env': 'prod',
// 'app-key': 'android_hd',
// 'user-agent': userAgent,
// 'x-bili-trace-id': traceId,
// 'x-bili-aurora-eid': '',
// 'x-bili-aurora-zone': '',
// 'bili-http-engine': 'cronet',
// 'content-type': 'application/x-www-form-urlencoded; charset=utf-8',
// };
// var res = await Request().post(Api.getGuestId,
// queryParameters: {...params, 'sign': sign},
// options: Options(
// contentType: Headers.formUrlEncodedContentType,
// headers: headers,
// ));
// print("getGuestId: $res");
// if (res.data['code'] == 0) {
// return {'status': true, 'data': res.data['data']};
// } else {
// return {'status': false, 'msg': res.data['message']};
// }
// }
// web端验证码登录
static Future loginInByWebSmsCode() async {}
// web端密码登录
static Future liginInByWebPwd() async {}
// app端验证码
static Future sendAppSmsCode({
int? cid,
required int tel,
required String token,
required String challenge,
required String validate,
required String seccode,
}) async {
Map<String, dynamic> data = {
'cid': cid,
'tel': tel,
'login_session_id': const Uuid().v4().replaceAll('-', ''),
'recaptcha_token': token,
'gee_challenge': challenge,
'gee_validate': validate,
'gee_seccode': seccode,
'channel': 'bili',
'buvid': buvid(),
'local_id': buvid(),
// 'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000,
'statistics': {
"appId": 1,
"platform": 3,
"version": "7.52.0",
"abtest": ""
},
};
// FormData formData = FormData.fromMap({...data});
var res = await Request().post(
Api.appSmsCode,
data: data,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
);
print(res);
}
static String buvid() {
static String genBuvid() {
var mac = <String>[];
var random = Random();
@@ -137,40 +227,215 @@ class LoginHttp {
return 'XY${md5Arr[2]}${md5Arr[12]}${md5Arr[22]}$md5Str';
}
// 获取盐hash跟PubKey
static Future getWebKey() async {
var res = await Request().get(Api.getWebKey,
data: {'disable_rcmd': 0, 'local_id': LoginUtils.generateBuvid()});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': {}, 'msg': res.data['message']};
}
static String genDeviceId() {
// https://github.com/bilive/bilive_client/blob/2873de0532c54832f5464a4c57325ad9af8b8698/bilive/lib/app_client.ts#L62
final String yyyyMMddHHmmss = DateTime.now()
.toIso8601String()
.replaceAll(RegExp(r'[-:TZ]'), '')
.substring(0, 14);
final Random random = Random(); // Random.secure();
final String randomHex32 =
List.generate(32, (index) => random.nextInt(16).toRadixString(16))
.join();
final String randomHex16 =
List.generate(16, (index) => random.nextInt(16).toRadixString(16))
.join();
final String deviceID = randomHex32 + yyyyMMddHHmmss + randomHex16;
final List<int> bytes = RegExp(r'\w{2}')
.allMatches(deviceID)
.map((match) => int.parse(match.group(0)!, radix: 16))
.toList();
final int checksumValue = bytes.reduce((a, b) => a + b);
final String check = checksumValue
.toRadixString(16)
.substring(checksumValue.toRadixString(16).length - 2);
return deviceID + check;
}
static String generateRandomString(int length) {
const chars =
'123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
final Random random = Random(); // Random.secure();
return List.generate(length, (index) => chars[random.nextInt(chars.length)])
.join();
}
// app端密码登录
static Future loginInByMobPwd({
required String tel,
static Future loginByPwd({
required String username,
required String password,
required String key,
required String rhash,
required String salt,
String? gee_challenge,
String? gee_seccode,
String? gee_validate,
String? recaptcha_token,
}) async {
dynamic publicKey = RSAKeyParser().parse(key);
String passwordEncryptyed =
Encrypter(RSA(publicKey: publicKey)).encrypt(rhash + password).base64;
print(publicKey);
String passwordEncrypted =
Encrypter(RSA(publicKey: publicKey)).encrypt(salt + password).base64;
Map<String, dynamic> data = {
'username': tel,
'password': passwordEncryptyed,
'local_id': LoginUtils.generateBuvid(),
'disable_rcmd': "0",
'appkey': Constants.appKey,
'bili_local_id': deviceId,
'build': '1462100',
'buvid': buvid,
'c_locale': 'zh_CN',
'channel': 'yingyongbao',
'device': 'phone',
'device_id': deviceId,
//'device_meta': '',
'device_name': 'vivo',
'device_platform': 'Android14vivo',
'disable_rcmd': '0',
'dt': Uri.encodeComponent(Encrypter(RSA(publicKey: publicKey))
.encrypt(generateRandomString(16))
.base64),
'from_pv': 'main.homepage.avatar-nologin.all.click',
'from_url': Uri.encodeComponent('bilibili://pegasus/promo'),
if (gee_challenge != null) 'gee_challenge': gee_challenge,
if (gee_seccode != null) 'gee_seccode': gee_seccode,
if (gee_validate != null) 'gee_validate': gee_validate,
'local_id': buvid, //LoginUtils.generateBuvid(),
'mobi_app': 'android_hd',
'password': passwordEncrypted,
'permission': 'ALL',
'platform': 'android',
if (recaptcha_token != null) 'recaptcha_token': recaptcha_token,
's_locale': 'zh_CN',
'statistics': statistics,
'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(),
'username': username,
};
String sign = Utils.appSign(
data,
Constants.appKey,
Constants.appSec,
);
data['sign'] = sign;
data.map((key, value) {
print('$key: $value');
return MapEntry<String, dynamic>(key, value);
});
final Map<String, String> headers = {
'Host': host,
'buvid': buvid,
'env': 'prod',
'app-key': 'android_hd',
'user-agent': userAgent,
'x-bili-trace-id': traceId,
'x-bili-aurora-eid': '',
'x-bili-aurora-zone': '',
'bili-http-engine': 'cronet',
'content-type': 'application/x-www-form-urlencoded; charset=utf-8',
};
var res = await Request().post(
Api.loginInByPwdApi,
Api.loginByPwdApi,
data: data,
options: Options(
contentType: Headers.formUrlEncodedContentType,
headers: headers,
//responseType: ResponseType.plain
),
);
print(res);
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {
'status': false,
'code': res.data['code'],
'msg': res.data['message'],
'data': res.data['data']
};
}
}
// app端短信验证码登录
static Future loginBySms({
required String captchaKey,
required String tel,
required String code,
required String cid,
required String key,
}) async {
dynamic publicKey = RSAKeyParser().parse(key);
Map<String, dynamic> data = {
'appkey': Constants.appKey,
'bili_local_id': deviceId,
'build': '1462100',
'buvid': buvid,
'c_locale': 'zh_CN',
'captcha_key': captchaKey,
'channel': 'yingyongbao',
'cid': cid,
'code': code,
'device': 'phone',
'device_id': deviceId,
//'device_meta': '',
'device_name': 'vivo',
'device_platform': 'Android14vivo',
// 'device_tourist_id': '',
'disable_rcmd': '0',
'dt': Uri.encodeComponent(Encrypter(RSA(publicKey: publicKey))
.encrypt(generateRandomString(16))
.base64),
'from_pv': 'main.my-information.my-login.0.click',
'from_url': Uri.encodeComponent('bilibili://user_center/mine'),
'local_id': buvid,
'mobi_app': 'android_hd',
'platform': 'android',
's_locale': 'zh_CN',
'statistics': statistics,
'tel': tel,
'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(),
};
String sign = Utils.appSign(
data,
Constants.appKey,
Constants.appSec,
);
data['sign'] = sign;
data.map((key, value) {
print('$key: $value');
return MapEntry<String, dynamic>(key, value);
});
final Map<String, String> headers = {
'Host': host,
'buvid': buvid,
'env': 'prod',
'app-key': 'android_hd',
'user-agent': userAgent,
'x-bili-trace-id': traceId,
'x-bili-aurora-eid': '',
'x-bili-aurora-zone': '',
'bili-http-engine': 'cronet',
'content-type': 'application/x-www-form-urlencoded; charset=utf-8',
};
var res = await Request().post(
Api.logInByAppSms,
data: data,
options: Options(
contentType: Headers.formUrlEncodedContentType,
headers: headers,
//responseType: ResponseType.plain
),
);
print(res);
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {
'status': false,
'code': res.data['code'],
'msg': res.data['message'],
'data': res.data['data']
};
}
}
}

View File

@@ -1,6 +1,3 @@
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart';
import '../common/constants.dart';
import '../models/dynamics/result.dart';
import '../models/follow/result.dart';
import '../models/member/archive.dart';
@@ -8,7 +5,6 @@ import '../models/member/coin.dart';
import '../models/member/info.dart';
import '../models/member/seasons.dart';
import '../models/member/tags.dart';
import '../utils/storage.dart';
import '../utils/utils.dart';
import '../utils/wbi_sign.dart';
import 'index.dart';
@@ -375,125 +371,6 @@ class MemberHttp {
}
}
// 获取TV authCode
static Future getTVCode() async {
SmartDialog.showLoading(msg: "正在申请HD版二维码...");
var params = {
'appkey': Constants.appKey,
// 'local_id': 'Y952A395BB157D305D8A8340FC2AAECECE17',
'local_id': '0',
'ts': DateTime.now().millisecondsSinceEpoch.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});
SmartDialog.dismiss();
print(res.data);
if (res.data['code'] == 0) {
print("getTVCode");
return {
'status': true,
'data': res.data['data']['auth_code'],
'msg': '操作成功'
};
} else {
return {
'status': false,
'data': [],
'msg': res.data,
};
}
}
// 获取access_key
static Future cookieToKey() async {
var authCodeRes = await getTVCode();
if (authCodeRes['status']) {
SmartDialog.showLoading(msg: "正在确认登录...");
var confirmRes =
await Request().post(Api.qrcodeConfirm, queryParameters: {
'auth_code': authCodeRes['data'],
'local_id': '0',
'build': 1442100,
'scanning_type': 1,
'csrf': await Request.getCsrf(),
});
print("confirmRes");
print(confirmRes);
SmartDialog.dismiss();
if (confirmRes.data['code'] != 0) {
return {
'status': false,
'data': [],
'msg':
"确认登录失败:${confirmRes.data['message']}\n\n请在设置中退出账号重启app重新登录再试",
};
}
SmartDialog.showLoading(msg: "等待500毫秒...");
await Future.delayed(const Duration(milliseconds: 500));
SmartDialog.dismiss();
SmartDialog.showLoading(msg: "正在获取登录结果含access_key...");
var res = await qrcodePoll(authCodeRes['data']);
SmartDialog.dismiss();
if (res['status']) {
return {'status': true, 'data': [], 'msg': res['msg']};
} else {
return {
'status': false,
'data': [],
'msg': "登录结果获取失败:${res.data['msg']}",
};
}
} else {
return {
'status': false,
'data': [],
'msg': "TV版二维码申请失败${authCodeRes['msg']}",
};
}
}
static Future qrcodePoll(authCode) async {
var params = {
'appkey': Constants.appKey,
'auth_code': authCode.toString(),
'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});
if (res.data['code'] == 0) {
String accessKey = res.data['data']['access_token'];
Box localCache = GStorage.localCache;
Box userInfoCache = GStorage.userInfo;
var userInfo = userInfoCache.get('userInfoCache');
localCache.put(
LocalCacheKey.accessKey, {'mid': userInfo.mid, 'value': accessKey});
return {
'status': true,
'data': [],
'msg': '操作成功当前获取的access_key为$accessKey'
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 获取up播放数、点赞数
static Future memberView({required int mid}) async {
var res = await Request().get(Api.getMemberViewApi, data: {'mid': mid});

View File

@@ -325,7 +325,7 @@ class VideoHttp {
String? accessKey = GStorage.localCache
.get(LocalCacheKey.accessKey, defaultValue: {})['value'];
if (accessKey == null || accessKey == "") {
return {'status': false, 'msg': "本操作使用app端接口请前往【隐私设置】刷新access_key"};
return {'status': false, 'msg': "请退出账号后重新登录"};
}
var res = await Request().post(
Api.dislikeVideo,
@@ -355,7 +355,7 @@ class VideoHttp {
String? accessKey = GStorage.localCache
.get(LocalCacheKey.accessKey, defaultValue: {})['value'];
if (accessKey == null || accessKey == "") {
return {'status': false, 'msg': "本操作使用app端接口请前往【隐私设置】刷新access_key"};
return {'status': false, 'msg': "请退出账号后重新登录"};
}
assert((reasonId != null) ^ (feedbackId != null));
var res = await Request().get(Api.feedDislike, data: {
@@ -386,7 +386,7 @@ class VideoHttp {
String? accessKey = GStorage.localCache
.get(LocalCacheKey.accessKey, defaultValue: {})['value'];
if (accessKey == null || accessKey == "") {
return {'status': false, 'msg': "本操作使用app端接口请前往【隐私设置】刷新access_key"};
return {'status': false, 'msg': "请退出账号后重新登录"};
}
// assert ((reasonId != null) ^ (feedbackId != null));
var res = await Request().get(Api.feedDislikeCancel, data: {

View File

@@ -1,204 +1,439 @@
import 'dart:async';
import 'dart:io';
import 'package:PiliPalaX/common/constants.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:PiliPalaX/http/login.dart';
import 'package:gt3_flutter_plugin/gt3_flutter_plugin.dart';
import 'package:PiliPalaX/models/login/index.dart';
import '../../utils/login.dart';
import 'package:hive/hive.dart';
import 'package:webview_cookie_manager/webview_cookie_manager.dart';
class LoginPageController extends GetxController {
final GlobalKey mobFormKey = GlobalKey<FormState>();
final GlobalKey passwordFormKey = GlobalKey<FormState>();
final GlobalKey msgCodeFormKey = GlobalKey<FormState>();
import '../../http/constants.dart';
import '../../http/init.dart';
import '../../http/user.dart';
import '../../utils/storage.dart';
import '../home/controller.dart';
import '../media/controller.dart';
final TextEditingController mobTextController = TextEditingController();
class LoginPageController extends GetxController
with GetSingleTickerProviderStateMixin {
final TextEditingController telTextController = TextEditingController();
final TextEditingController usernameTextController = TextEditingController();
final TextEditingController passwordTextController = TextEditingController();
final TextEditingController msgCodeTextController = TextEditingController();
final TextEditingController smsCodeTextController = TextEditingController();
final FocusNode mobTextFieldNode = FocusNode();
final FocusNode passwordTextFieldNode = FocusNode();
final FocusNode msgCodeTextFieldNode = FocusNode();
Rx<Map<String, dynamic>> codeInfo = Rx<Map<String, dynamic>>({});
final PageController pageViewController = PageController();
RxInt currentIndex = 0.obs;
late TabController tabController;
final Gt3FlutterPlugin captcha = Gt3FlutterPlugin();
// 默认密码登录
RxInt loginType = 0.obs;
CaptchaDataModel captchaData = CaptchaDataModel();
RxInt qrCodeLeftTime = 180.obs;
Rx<String> statusQRCode = ''.obs;
// 监听pageView切换
void onPageChange(int index) {
currentIndex.value = index;
Map<String, dynamic> selectedCountryCodeId =
Constants.internationalDialingPrefix.first;
String captchaKey = '';
RxInt smsSendCooldown = 0.obs;
int smsSendTimestamp = 0;
// 定时器
Timer? qrCodeTimer;
Timer? smsSendCooldownTimer;
@override
void onInit() {
super.onInit();
tabController = TabController(length: 3, vsync: this)
..addListener(_handleTabChange);
}
// 输入手机号 下一页
void nextStep() async {
if ((mobFormKey.currentState as FormState).validate()) {
await pageViewController.animateToPage(
1,
duration: const Duration(microseconds: 3000),
curve: Curves.easeInOut,
);
passwordTextFieldNode.requestFocus();
}
@override
void onClose() {
tabController.removeListener(_handleTabChange);
tabController.dispose();
qrCodeTimer?.cancel();
smsSendCooldownTimer?.cancel();
super.onClose();
}
// 上一页
void previousPage() async {
passwordTextFieldNode.unfocus();
await Future.delayed(const Duration(milliseconds: 200));
pageViewController.animateToPage(
0,
duration: const Duration(microseconds: 300),
curve: Curves.easeInOut,
);
}
void refreshQRCode() {
LoginHttp.getHDcode().then((res) {
if (res['status']) {
qrCodeTimer?.cancel();
codeInfo.value = res;
codeInfo.refresh();
print("codeInfo");
print(codeInfo);
qrCodeTimer = Timer.periodic(const Duration(milliseconds: 1000), (t) {
qrCodeLeftTime.value = 180 - t.tick;
if (qrCodeLeftTime <= 0) {
t.cancel();
statusQRCode.value = '二维码已过期,请刷新';
qrCodeLeftTime = 0.obs;
return;
}
// 切换登录方式
void changeLoginType() {
loginType.value = loginType.value == 0 ? 1 : 0;
if (loginType.value == 0) {
passwordTextFieldNode.requestFocus();
} else {
msgCodeTextFieldNode.requestFocus();
}
}
// app端密码登录
void loginInByAppPassword() async {
if ((passwordFormKey.currentState as FormState).validate()) {
var webKeyRes = await LoginHttp.getWebKey();
if (webKeyRes['status']) {
String rhash = webKeyRes['data']['hash'];
String key = webKeyRes['data']['key'];
LoginHttp.loginInByMobPwd(
tel: mobTextController.text,
password: passwordTextController.text,
key: key,
rhash: rhash,
);
LoginHttp.codePoll(codeInfo.value['data']['auth_code'])
.then((value) async {
if (value['status']) {
t.cancel();
statusQRCode.value = '扫码成功';
print(value['data']);
await afterLoginByApp(
value['data'], value['data']['cookie_info']);
Get.back();
} else if (value['code'] == 86038) {
t.cancel();
qrCodeLeftTime = 0.obs;
} else {
statusQRCode.value = value['msg'];
}
});
});
} else {
SmartDialog.showToast(webKeyRes['msg']);
SmartDialog.showToast(res['msg']);
}
});
}
void _handleTabChange() {
print('tabController.index ${tabController.index}');
if (tabController.index == 2) {
if (qrCodeTimer == null || qrCodeTimer!.isActive == false) {
refreshQRCode();
}
}
}
// 验证码登录
void loginInByCode() {
if ((msgCodeFormKey.currentState as FormState).validate()) {}
}
// app端验证码
void getMsgCode() async {
getCaptcha((data) async {
CaptchaDataModel captchaData = data;
var res = await LoginHttp.sendAppSmsCode(
cid: 86,
tel: 13734077064,
token: captchaData.token!,
challenge: captchaData.geetest!.challenge!,
validate: captchaData.validate!,
seccode: captchaData.seccode!,
);
print(res);
Future afterLoginByApp(Map<String, dynamic> token_info, cookie_info) async {
Box localCache = GStorage.localCache;
localCache.put(LocalCacheKey.accessKey, {
'mid': token_info['mid'],
'value': token_info['access_token'],
'refresh': token_info['refresh_token']
});
List<dynamic> cookieInfo = cookie_info['cookies'];
print("cookieInfo");
print(cookieInfo);
List<Cookie> cookies = [];
String cookieStrings = cookieInfo.map((cookie) {
String cstr =
'${cookie['name']}=${cookie['value']};Domain=.bilibili.com;Path=/;';
cookies.add(Cookie.fromSetCookieValue(cstr));
return cstr;
}).join('');
List<String> Urls = [
HttpString.baseUrl,
HttpString.apiBaseUrl,
HttpString.tUrl
];
for (var url in Urls) {
await Request.cookieManager.cookieJar
.saveFromResponse(Uri.parse(url), cookies);
}
print(cookieStrings);
print(Request.cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.apiBaseUrl)));
Request.dio.options.headers['cookie'] = cookieStrings;
print(Request.dio.options);
try {
await WebviewCookieManager().setCookies(cookies);
} catch (e) {
SmartDialog.showToast('webview设置cookie失败$e');
}
final result = await UserHttp.userInfo();
if (result['status'] && result['data'].isLogin) {
SmartDialog.showToast('登录成功,当前采用「'
'${GStorage.setting.get(SettingBoxKey.defaultRcmdType, defaultValue: 'web')}'
'端」推荐');
Box userInfoCache = GStorage.userInfo;
await userInfoCache.put('userInfoCache', result['data']);
final HomeController homeCtr = Get.find<HomeController>();
homeCtr.updateLoginStatus(true);
homeCtr.userFace.value = result['data'].face;
final MediaController mediaCtr = Get.find<MediaController>();
mediaCtr.mid = result['data'].mid;
await LoginUtils.refreshLoginStatus(true);
} else {
// 获取用户信息失败
SmartDialog.showNotify(
msg: '登录失败请检查cookie是否正确${result['message']}',
notifyType: NotifyType.warning);
}
}
// 申请极验验证码
Future getCaptcha(oncall) async {
SmartDialog.showLoading(msg: '请求中...');
var result = await LoginHttp.queryCaptcha();
SmartDialog.dismiss();
if (result['status']) {
CaptchaDataModel captchaData = result['data'];
var registerData = Gt3RegisterData(
challenge: captchaData.geetest!.challenge,
gt: captchaData.geetest!.gt!,
success: true,
);
captcha.addEventHandler(onShow: (Map<String, dynamic> message) async {
}, onClose: (Map<String, dynamic> message) async {
SmartDialog.showToast('关闭验证');
}, onResult: (Map<String, dynamic> message) async {
debugPrint("Captcha result: $message");
String code = message["code"];
if (code == "1") {
// 发送 message["result"] 中的数据向 B 端的业务服务接口进行查询
SmartDialog.showToast('验证成功');
captchaData.validate = message['result']['geetest_validate'];
captchaData.seccode = message['result']['geetest_seccode'];
captchaData.geetest!.challenge =
message['result']['geetest_challenge'];
oncall(captchaData);
} else {
// 终端用户完成验证失败,自动重试 If the verification fails, it will be automatically retried.
debugPrint("Captcha result code : $code");
}
}, onError: (Map<String, dynamic> message) async {
String code = message["code"];
Future getCaptcha(geeGt, geeChallenge, onSuccess) async {
var registerData = Gt3RegisterData(
challenge: geeChallenge,
gt: geeGt,
success: true,
);
// 处理验证中返回的错误 Handling errors returned in verification
if (Platform.isAndroid) {
// Android 平台
if (code == "-2") {
// Dart 调用异常 Call exception
} else if (code == "-1") {
// Gt3RegisterData 参数不合法 Parameter is invalid
} else if (code == "201") {
// 网络无法访问 Network inaccessible
} else if (code == "202") {
// Json 解析错误 Analysis error
} else if (code == "204") {
// WebView 加载超时,请检查是否混淆极验 SDK Load timed out
} else if (code == "204_1") {
// WebView 加载前端页面错误,请查看日志 Error loading front-end page, please check the log
} else if (code == "204_2") {
// WebView 加载 SSLError
} else if (code == "206") {
// gettype 接口错误或返回为 null API error or return null
} else if (code == "207") {
// getphp 接口错误或返回为 null API error or return null
} else if (code == "208") {
// ajax 接口错误或返回为 null API error or return null
captcha.addEventHandler(
onShow: (Map<String, dynamic> message) async {},
onClose: (Map<String, dynamic> message) async {
SmartDialog.showToast('关闭验证');
},
onResult: (Map<String, dynamic> message) async {
debugPrint("Captcha result: $message");
String code = message["code"];
if (code == "1") {
// 发送 message["result"] 中的数据向 B 端的业务服务接口进行查询
SmartDialog.showToast('验证成功');
captchaData.validate = message['result']['geetest_validate'];
captchaData.seccode = message['result']['geetest_seccode'];
captchaData.geetest = GeetestData(
challenge: message['result']['geetest_challenge'],
gt: geeGt,
);
onSuccess();
} else {
// 更多错误码参考开发文档 More error codes refer to the development document
// https://docs.geetest.com/sensebot/apirefer/errorcode/android
// 终端用户完成验证失败,自动重试 If the verification fails, it will be automatically retried.
debugPrint("Captcha result code : $code");
}
},
onError: (Map<String, dynamic> message) async {
SmartDialog.showToast("Captcha onError: $message");
String code = message["code"];
// 处理验证中返回的错误 Handling errors returned in verification
if (Platform.isAndroid) {
// Android 平台
if (code == "-2") {
// Dart 调用异常 Call exception
} else if (code == "-1") {
// Gt3RegisterData 参数不合法 Parameter is invalid
} else if (code == "201") {
// 网络无法访问 Network inaccessible
} else if (code == "202") {
// Json 解析错误 Analysis error
} else if (code == "204") {
// WebView 加载超时,请检查是否混淆极验 SDK Load timed out
} else if (code == "204_1") {
// WebView 加载前端页面错误,请查看日志 Error loading front-end page, please check the log
} else if (code == "204_2") {
// WebView 加载 SSLError
} else if (code == "206") {
// gettype 接口错误或返回为 null API error or return null
} else if (code == "207") {
// getphp 接口错误或返回为 null API error or return null
} else if (code == "208") {
// ajax 接口错误或返回为 null API error or return null
} else {
// 更多错误码参考开发文档 More error codes refer to the development document
// https://docs.geetest.com/sensebot/apirefer/errorcode/android
}
}
}
if (Platform.isIOS) {
// iOS 平台
if (code == "-1009") {
// 网络无法访问 Network inaccessible
} else if (code == "-1004") {
// 无法查找到 HOST Unable to find HOST
} else if (code == "-1002") {
// 非法的 URL Illegal URL
} else if (code == "-1001") {
// 网络超时 Network timeout
} else if (code == "-999") {
// 请求被意外中断, 一般由用户进行取消操作导致 The interrupted request was usually caused by the user cancelling the operation
} else if (code == "-21") {
// 使用了重复的 challenge Duplicate challenges are used
// 检查获取 challenge 是否进行了缓存 Check if the fetch challenge is cached
} else if (code == "-20") {
// 尝试过多, 重新引导用户触发验证即可 Try too many times, lead the user to request verification again
} else if (code == "-10") {
// 预判断时被封禁, 不会再进行图形验证 Banned during pre-judgment, and no more image captcha verification
} else if (code == "-2") {
// Dart 调用异常 Call exception
} else if (code == "-1") {
// Gt3RegisterData 参数不合法 Parameter is invalid
} else {
// 更多错误码参考开发文档 More error codes refer to the development document
// https://docs.geetest.com/sensebot/apirefer/errorcode/ios
if (Platform.isIOS) {
// iOS 平台
if (code == "-1009") {
// 网络无法访问 Network inaccessible
} else if (code == "-1004") {
// 无法查找到 HOST Unable to find HOST
} else if (code == "-1002") {
// 非法的 URL Illegal URL
} else if (code == "-1001") {
// 网络超时 Network timeout
} else if (code == "-999") {
// 请求被意外中断, 一般由用户进行取消操作导致 The interrupted request was usually caused by the user cancelling the operation
} else if (code == "-21") {
// 使用了重复的 challenge Duplicate challenges are used
// 检查获取 challenge 是否进行了缓存 Check if the fetch challenge is cached
} else if (code == "-20") {
// 尝试过多, 重新引导用户触发验证即可 Try too many times, lead the user to request verification again
} else if (code == "-10") {
// 预判断时被封禁, 不会再进行图形验证 Banned during pre-judgment, and no more image captcha verification
} else if (code == "-2") {
// Dart 调用异常 Call exception
} else if (code == "-1") {
// Gt3RegisterData 参数不合法 Parameter is invalid
} else {
// 更多错误码参考开发文档 More error codes refer to the development document
// https://docs.geetest.com/sensebot/apirefer/errorcode/ios
}
}
});
captcha.startCaptcha(registerData);
}
// app端密码登录
void loginByPassword() async {
String username = usernameTextController.text;
String password = passwordTextController.text;
if (username.isEmpty || password.isEmpty) {
SmartDialog.showToast('用户名或密码不能为空');
return;
}
// if ((passwordFormKey.currentState as FormState).validate()) {
var webKeyRes = await LoginHttp.getWebKey();
print(webKeyRes);
if (!webKeyRes['status']) {
SmartDialog.showToast(webKeyRes['msg']);
return;
}
String salt = webKeyRes['data']['hash'];
String key = webKeyRes['data']['key'];
print(key);
var res = await LoginHttp.loginByPwd(
username: username,
password: password,
key: key,
salt: salt,
gee_validate: captchaData.validate,
gee_seccode: captchaData.seccode,
gee_challenge: captchaData.geetest?.challenge,
recaptcha_token: captchaData.token,
);
print(res);
if (res['status']) {
SmartDialog.showToast('登录成功');
var data = res['data'];
for (var key in data.keys) {
print('$key: ${data[key]}');
}
await afterLoginByApp(data['token_info'], data['cookie_info']);
Get.back();
} else {
// handle login result
switch (res['code']) {
case 0:
// login success
break;
case -105:
String captureUrl = res['data']['url'];
Uri captureUri = Uri.parse(captureUrl);
captchaData.token = captureUri.queryParameters['recaptcha_token']!;
String geeGt = captureUri.queryParameters['gee_gt']!;
String geeChallenge = captureUri.queryParameters['gee_challenge']!;
getCaptcha(geeGt, geeChallenge, () {
loginByPassword();
});
break;
default:
SmartDialog.showToast(res['msg']);
// login failed
break;
}
}
// }
}
// 短信验证码登录
void loginBySmsCode() async {
if (telTextController.text.isEmpty) {
SmartDialog.showToast('手机号不能为空');
return;
}
if (captchaKey.isEmpty) {
SmartDialog.showToast('请先点击获取验证码');
return;
}
if (smsCodeTextController.text.isEmpty) {
SmartDialog.showToast('验证码不能为空');
return;
}
if (DateTime.now().millisecondsSinceEpoch - smsSendTimestamp >
1000 * 60 * 5) {
SmartDialog.showToast('验证码已过期,请重新获取');
return;
}
var webKeyRes = await LoginHttp.getWebKey();
if (!webKeyRes['status']) {
SmartDialog.showToast(webKeyRes['msg']);
return;
}
String key = webKeyRes['data']['key'];
var res = await LoginHttp.loginBySms(
tel: telTextController.text,
code: smsCodeTextController.text,
captchaKey: captchaKey,
cid: selectedCountryCodeId['country_id'],
key: key,
);
print(res);
if (res['status']) {
SmartDialog.showToast('登录成功');
var data = res['data'];
for (var key in data.keys) {
print('$key: ${data[key]}');
}
await afterLoginByApp(data['token_info'], data['cookie_info']);
Get.back();
} else {
SmartDialog.showToast(res['msg']);
}
}
// app端验证码
void sendSmsCode() async {
if (telTextController.text.isEmpty) {
SmartDialog.showToast('手机号不能为空');
return;
}
// String? guestId;
// var webKeyRes = await LoginHttp.getWebKey();
// if (!webKeyRes['status']) {
// SmartDialog.showToast(webKeyRes['msg']);
// } else {
// String key = webKeyRes['data']['key'];
// var guestIdRes = await LoginHttp.getGuestId(key);
// if (!guestIdRes['status']) {
// SmartDialog.showToast(guestIdRes['msg']);
// } else {
// guestId = guestIdRes['data']['guest_id'];
// }
// }
var res = await LoginHttp.sendSmsCode(
tel: telTextController.text,
cid: selectedCountryCodeId['country_id'],
// deviceTouristId: guestId,
gee_validate: captchaData.validate,
gee_seccode: captchaData.seccode,
gee_challenge: captchaData.geetest?.challenge,
recaptcha_token: captchaData.token,
);
print(res);
if (res['status']) {
SmartDialog.showToast('发送成功');
smsSendTimestamp = DateTime.now().millisecondsSinceEpoch;
smsSendCooldown.value = 60;
captchaKey = res['data']['captcha_key'];
smsSendCooldownTimer = Timer.periodic(Duration(seconds: 1), (timer) {
smsSendCooldown.value = 60 - timer.tick;
if (smsSendCooldown <= 0) {
smsSendCooldownTimer?.cancel();
smsSendCooldown.value = 0;
}
});
captcha.startCaptcha(registerData);
} else {}
} else {
// handle login result
switch (res['code']) {
case 0:
case -105:
String captureUrl = res['data']['recaptcha_url'];
Uri captureUri = Uri.parse(captureUrl);
captchaData.token = captureUri.queryParameters['recaptcha_token']!;
String geeGt = captureUri.queryParameters['gee_gt']!;
String geeChallenge = captureUri.queryParameters['gee_challenge']!;
getCaptcha(geeGt, geeChallenge, () {
sendSmsCode();
});
break;
default:
SmartDialog.showToast(res['msg']);
// login failed
break;
}
}
}
}

View File

@@ -1,5 +1,14 @@
import 'dart:ui';
import 'package:PiliPalaX/common/constants.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:saver_gallery/saver_gallery.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'controller.dart';
@@ -12,355 +21,463 @@ class LoginPage extends StatefulWidget {
class _LoginPageState extends State<LoginPage> {
final LoginPageController _loginPageCtr = Get.put(LoginPageController());
// late Future<Map<String, dynamic>> codeFuture;
// 二维码生成时间
bool showPassword = false;
GlobalKey globalKey = GlobalKey();
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: Obx(
() => _loginPageCtr.currentIndex.value == 0
? IconButton(
tooltip: '关闭',
onPressed: () async {
_loginPageCtr.mobTextFieldNode.unfocus();
await Future.delayed(const Duration(milliseconds: 200));
Get.back();
},
icon: const Icon(Icons.close_outlined),
)
: IconButton(
tooltip: '返回',
onPressed: () => _loginPageCtr.previousPage(),
icon: const Icon(Icons.arrow_back),
),
),
),
body: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: _loginPageCtr.pageViewController,
onPageChanged: (int index) => _loginPageCtr.onPageChange(index),
children: [
Padding(
padding: EdgeInsets.only(
left: 20,
right: 20,
top: 10,
bottom: MediaQuery.of(context).padding.bottom + 10,
void dispose() {
_loginPageCtr.dispose();
super.dispose();
}
Widget loginByQRCode() {
return Column(
children: [
const SizedBox(height: 20),
const Text('使用 bilibili 官方 App 扫码登录'),
const SizedBox(height: 20),
Obx(() => Text('剩余有效时间: ${_loginPageCtr.qrCodeLeftTime}',
style:
const TextStyle(fontFeatures: [FontFeature.tabularFigures()]))),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// const SizedBox(width: 20),
TextButton.icon(
onPressed: _loginPageCtr.refreshQRCode,
icon: const Icon(Icons.refresh),
label: const Text('刷新二维码'),
),
child: Form(
key: _loginPageCtr.mobFormKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Text(
'登录',
style: Theme.of(context).textTheme.titleLarge!.copyWith(
letterSpacing: 1,
height: 2.1,
fontSize: 34,
fontWeight: FontWeight.w500),
TextButton.icon(
onPressed: () async {
SmartDialog.showLoading(msg: '正在生成截图');
RenderRepaintBoundary boundary = globalKey.currentContext!
.findRenderObject()! as RenderRepaintBoundary;
var image = await boundary.toImage();
ByteData? byteData =
await image.toByteData(format: ImageByteFormat.png);
Uint8List pngBytes = byteData!.buffer.asUint8List();
SmartDialog.dismiss();
SmartDialog.showLoading(msg: '正在保存至图库');
String picName =
"PiliPalaX_loginQRCode_${DateTime.now().toString().replaceAll(' ', '_').replaceAll(':', '-').split('.').first}";
final SaveResult result = await SaverGallery.saveImage(
Uint8List.fromList(pngBytes),
name: picName,
fileExtension: 'png',
// 保存到 PiliPalaX文件夹
androidRelativePath: "Pictures/PiliPalaX",
androidExistNotSave: false,
);
SmartDialog.dismiss();
if (result.isSuccess) {
await SmartDialog.showToast('$picName」已保存 ');
} else {
await SmartDialog.showToast('保存失败,${result.errorMessage}');
}
},
icon: const Icon(Icons.save),
label: const Text('保存至相册'),
),
],
),
RepaintBoundary(
key: globalKey,
child: Obx(() => QrImageView(
backgroundColor: Theme.of(context).colorScheme.background,
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.square,
color: Theme.of(context).colorScheme.primary,
),
Row(
children: [
Text(
'请使用您的 BiliBili 账号登录。',
style: Theme.of(context).textTheme.titleSmall!,
),
GestureDetector(
onTap: () {},
child: const Icon(Icons.info_outline, size: 16),
)
],
dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: Theme.of(context).colorScheme.secondary,
),
Container(
margin: const EdgeInsets.only(top: 38, bottom: 15),
child: TextFormField(
controller: _loginPageCtr.mobTextController,
focusNode: _loginPageCtr.mobTextFieldNode,
keyboardType: TextInputType.number,
decoration: InputDecoration(
isDense: true,
labelText: '输入手机号码',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6.0),
),
),
// 校验用户名
validator: (v) {
return v!.trim().isNotEmpty ? null : "手机号码不能为空";
},
onSaved: (val) {
print(val);
},
onEditingComplete: () {
_loginPageCtr.nextStep();
},
),
),
GestureDetector(
onTap: () {
Get.offNamed(
'/webview',
parameters: {
'url':
'https://passport.bilibili.com/h5-app/passport/login',
'type': 'login',
'pageTitle': '登录bilibili',
},
);
},
child: Padding(
padding: const EdgeInsets.only(left: 2),
child: Text(
'使用网页端登录',
style: TextStyle(
color: Theme.of(context).colorScheme.primary),
),
),
),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(onPressed: () {}, child: const Text('中国大陆')),
TextButton(
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
foregroundColor:
Theme.of(context).colorScheme.onPrimary,
backgroundColor:
Theme.of(context).colorScheme.primary, // 设置按钮背景色
),
onPressed: () => _loginPageCtr.nextStep(),
child: const Text('下一步'),
)
],
),
],
data: _loginPageCtr.codeInfo.value['data']?['url'] ?? "",
size: 200,
semanticsLabel: '二维码',
))),
const SizedBox(height: 10),
Obx(() => Text(_loginPageCtr.statusQRCode.value)),
Obx(() => GestureDetector(
onTap: () {
//以外部方式打开此链接
launchUrlString(
_loginPageCtr.codeInfo.value['data']?['url'] ?? "",
mode: LaunchMode.externalApplication);
},
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
child: Text(_loginPageCtr.codeInfo.value['data']?['url'] ?? "",
style: Theme.of(context).textTheme.labelSmall!.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.4))),
),
)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text('请务必在 PiliPalaX 开源仓库等可信渠道下载安装。',
style: Theme.of(context).textTheme.labelSmall!.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.4)))),
],
);
}
Widget loginByPassword() {
return Column(
children: [
const SizedBox(height: 20),
const Text('使用账号密码登录'),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: TextField(
controller: _loginPageCtr.usernameTextController,
inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r"\s"))],
decoration: InputDecoration(
prefixIcon: const Icon(Icons.account_box),
border: const UnderlineInputBorder(),
labelText: '账号',
hintText: '邮箱/手机号',
suffixIcon: IconButton(
onPressed: _loginPageCtr.usernameTextController.clear,
icon: const Icon(Icons.clear),
),
),
),
Padding(
padding: EdgeInsets.only(
left: 20,
right: 20,
top: 10,
bottom: MediaQuery.of(context).padding.bottom + 10,
),
child: Obx(
() => _loginPageCtr.loginType.value == 0
? Form(
key: _loginPageCtr.passwordFormKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Row(
children: [
Text(
'密码登录',
style: Theme.of(context)
.textTheme
.titleLarge!
.copyWith(
letterSpacing: 1,
height: 2.1,
fontSize: 34,
fontWeight: FontWeight.w500),
),
const SizedBox(width: 4),
IconButton(
tooltip: '切换至验证码登录',
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.resolveWith(
(states) {
return Theme.of(context)
.colorScheme
.primary
.withOpacity(0.1);
}),
),
onPressed: () =>
_loginPageCtr.changeLoginType(),
icon: const Icon(Icons.swap_vert_outlined),
)
],
),
Text(
'请输入您的 BiliBili 密码。',
style: Theme.of(context).textTheme.titleSmall!,
),
Container(
margin: const EdgeInsets.only(top: 38, bottom: 15),
child: TextFormField(
controller: _loginPageCtr.passwordTextController,
focusNode: _loginPageCtr.passwordTextFieldNode,
keyboardType: TextInputType.visiblePassword,
decoration: InputDecoration(
isDense: true,
labelText: '输入密码',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6.0),
),
),
// 校验用户名
validator: (v) {
return v!.trim().isNotEmpty ? null : "密码不能为空";
},
onSaved: (val) {
print(val);
},
),
),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => _loginPageCtr.previousPage(),
child: const Text('上一步'),
),
const SizedBox(width: 15),
TextButton(
style: TextButton.styleFrom(
padding:
const EdgeInsets.fromLTRB(20, 0, 20, 0),
foregroundColor:
Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context)
.colorScheme
.primary, // 设置按钮背景色
),
onPressed: () =>
_loginPageCtr.loginInByAppPassword(),
child: const Text('确认登录'),
)
],
),
],
),
)
: Form(
key: _loginPageCtr.msgCodeFormKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Row(
children: [
Text(
'验证码登录',
style: Theme.of(context)
.textTheme
.titleLarge!
.copyWith(
letterSpacing: 1,
height: 2.1,
fontSize: 34,
fontWeight: FontWeight.w500),
),
const SizedBox(width: 4),
IconButton(
tooltip: '切换至密码登录',
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.resolveWith(
(states) {
return Theme.of(context)
.colorScheme
.primary
.withOpacity(0.1);
}),
),
onPressed: () =>
_loginPageCtr.changeLoginType(),
icon: const Icon(Icons.swap_vert_outlined),
)
],
),
Text(
'请输入收到到验证码。',
style: Theme.of(context).textTheme.titleSmall!,
),
Container(
margin: const EdgeInsets.only(top: 38, bottom: 15),
child: Stack(
children: [
TextFormField(
controller:
_loginPageCtr.msgCodeTextController,
focusNode: _loginPageCtr.msgCodeTextFieldNode,
maxLength: 6,
keyboardType: TextInputType.number,
decoration: InputDecoration(
isDense: true,
labelText: '输入验证码',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6.0),
),
),
// 校验用户名
validator: (v) {
return v!.trim().isNotEmpty
? null
: "验证码不能为空";
},
onSaved: (val) {
print(val);
},
),
Positioned(
right: 8,
top: 4,
child: Center(
child: TextButton(
onPressed: () =>
_loginPageCtr.getMsgCode(),
child: const Text('获取验证码'),
),
),
),
],
),
),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => _loginPageCtr.previousPage(),
child: const Text('上一步'),
),
const SizedBox(width: 15),
TextButton(
style: TextButton.styleFrom(
padding:
const EdgeInsets.fromLTRB(20, 0, 20, 0),
foregroundColor:
Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context)
.colorScheme
.primary, // 设置按钮背景色
),
onPressed: () => _loginPageCtr.loginInByCode(),
child: const Text('确认登录'),
)
],
),
],
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: TextField(
obscureText: !showPassword,
keyboardType: TextInputType.visiblePassword,
inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r"\s"))],
controller: _loginPageCtr.passwordTextController,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock),
border: const UnderlineInputBorder(),
labelText: '密码',
suffixIcon: IconButton(
onPressed: _loginPageCtr.passwordTextController.clear,
icon: const Icon(Icons.clear),
),
),
),
],
),
),
Row(
children: [
const SizedBox(width: 10),
Checkbox(
value: showPassword,
onChanged: (value) {
setState(() {
showPassword = value!;
});
},
),
const Text('显示密码'),
const Spacer(),
TextButton(
onPressed: () {
//https://passport.bilibili.com/h5-app/passport/login/findPassword
//https://passport.bilibili.com/passport/findPassword
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
title: const Text('忘记密码?'),
contentPadding:
const EdgeInsets.fromLTRB(0.0, 2.0, 0.0, 16.0),
children: [
const Padding(
padding: EdgeInsets.fromLTRB(25, 0, 25, 10),
child: Text("试试扫码、手机号登录,或选择")),
ListTile(
title: const Text(
'找回密码(手机版)',
),
leading: const Icon(Icons.smartphone_outlined),
subtitle: const Text(
'https://passport.bilibili.com/h5-app/passport/login/findPassword',
),
dense: false,
onTap: () async {
Get.back();
Get.toNamed('/webview', parameters: {
'url':
'https://passport.bilibili.com/h5-app/passport/login/findPassword',
'type': 'url',
'pageTitle': '忘记密码',
});
}),
ListTile(
title: const Text(
'找回密码(电脑版)',
),
leading: const Icon(Icons.desktop_windows_outlined),
subtitle: const Text(
'https://passport.bilibili.com/pc/passport/findPassword',
),
dense: false,
onTap: () async {
Get.back();
Get.toNamed('/webview', parameters: {
'url':
'https://passport.bilibili.com/pc/passport/findPassword',
'type': 'url',
'pageTitle': '忘记密码',
'uaType': 'pc'
});
}),
],
);
},
);
},
child: const Text('忘记密码'),
),
const SizedBox(width: 20),
],
),
OutlinedButton.icon(
onPressed: _loginPageCtr.loginByPassword,
icon: const Icon(Icons.login_outlined),
label: const Text('登录'),
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
'根据 bilibili 官方登录接口规范,密码将在本地加盐、加密后传输。\n'
'盐与公钥均由官方提供;以 RSA/ECB/PKCS1Padding 方式加密。\n'
'账号密码仅用于该登录接口,不予保存;本地仅存储登录凭证。\n'
'请务必在 PiliPalaX 开源仓库等可信渠道下载安装。',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.labelSmall!.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.4)))),
],
);
}
Widget loginBySmS() {
return Column(
children: [
const SizedBox(height: 20),
const Text('使用手机短信验证码登录'),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: Container(
decoration: UnderlineTabIndicator(
borderSide: BorderSide(
color:
Theme.of(context).colorScheme.outline.withOpacity(0.4)),
),
child: Row(
children: [
const SizedBox(width: 12),
const Icon(Icons.phone),
const SizedBox(width: 12),
PopupMenuButton<Map<String, dynamic>>(
padding: EdgeInsets.zero,
tooltip: '选择国际冠码,'
'当前为${_loginPageCtr.selectedCountryCodeId['cname']}'
'+${_loginPageCtr.selectedCountryCodeId['country_id']}',
//position: PopupMenuPosition.under,
onSelected: (Map<String, dynamic> type) {},
itemBuilder: (BuildContext context) => Constants
.internationalDialingPrefix
.map((Map<String, dynamic> item) {
return PopupMenuItem<Map<String, dynamic>>(
onTap: () {
setState(() {
_loginPageCtr.selectedCountryCodeId = item;
});
},
value: item,
// height: menuItemHeight,
child: Row(children: [
Text(item['cname']),
const Spacer(),
Text("+${item['country_id']}")
]),
);
}).toList(),
child: Text(
"+${_loginPageCtr.selectedCountryCodeId['country_id']}"),
),
const SizedBox(width: 6),
SizedBox(
height: 24, // 这里设置固定高度
child: VerticalDivider(
color: Theme.of(context)
.colorScheme
.outline
.withOpacity(0.5),
),
),
const SizedBox(width: 6),
Expanded(
child: TextField(
controller: _loginPageCtr.telTextController,
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly,
],
decoration: InputDecoration(
border: InputBorder.none,
labelText: '手机号',
suffixIcon: IconButton(
onPressed: _loginPageCtr.telTextController.clear,
icon: const Icon(Icons.clear),
),
),
)),
],
),
)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: Container(
decoration: UnderlineTabIndicator(
borderSide: BorderSide(
color:
Theme.of(context).colorScheme.outline.withOpacity(0.4)),
),
child: Row(
children: [
Expanded(
child: TextField(
controller: _loginPageCtr.smsCodeTextController,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.key),
border: InputBorder.none,
labelText: '验证码',
),
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly,
],
),
),
Obx(() => TextButton.icon(
onPressed: _loginPageCtr.smsSendCooldown > 0
? null
: _loginPageCtr.sendSmsCode,
icon: const Icon(Icons.send),
label: Text(_loginPageCtr.smsSendCooldown > 0
? '等待${_loginPageCtr.smsSendCooldown}'
: '获取验证码'),
)),
],
),
)),
const SizedBox(height: 20),
OutlinedButton.icon(
onPressed: _loginPageCtr.loginBySmsCode,
icon: const Icon(Icons.login_outlined),
label: const Text('登录'),
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
'手机号仅用于 bilibili 官方发送验证码与登录接口,不予保存;\n'
'本地仅存储登录凭证。\n'
'请务必在 PiliPalaX 开源仓库等可信渠道下载安装。',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.labelSmall!.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.4)))),
],
);
}
@override
Widget build(BuildContext context) {
return OrientationBuilder(builder: (context, orientation) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
leading: IconButton(
tooltip: '关闭',
icon: const Icon(Icons.close_outlined),
onPressed: Get.back),
title: Row(children: [
const Text('登录'),
if (orientation == Orientation.landscape) ...[
const Spacer(),
Flexible(
child: TabBar(
dividerHeight: 0,
tabs: const [
Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [Icon(Icons.lock), Text(' 密码')])),
Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [Icon(Icons.key), Text(' 短信')])),
Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [Icon(Icons.qr_code), Text(' 扫码')])),
],
controller: _loginPageCtr.tabController,
))
]
]),
bottom: orientation == Orientation.portrait
? TabBar(
tabs: const [
Tab(icon: Icon(Icons.lock), text: '密码'),
Tab(icon: Icon(Icons.key), text: '短信'),
Tab(icon: Icon(Icons.qr_code), text: '扫码'),
],
controller: _loginPageCtr.tabController,
)
: null,
),
body: TabBarView(
physics: const AlwaysScrollableScrollPhysics(),
controller: _loginPageCtr.tabController,
children: [
tabViewOuter(loginByPassword()),
tabViewOuter(loginBySmS()),
tabViewOuter(loginByQRCode()),
],
),
);
});
}
Widget tabViewOuter(child) {
return SingleChildScrollView(
child: Align(
alignment: Alignment.topCenter,
child: SizedBox(
height: 500,
width: 600,
child: child,
)));
}
}

View File

@@ -36,22 +36,20 @@ class MineController extends GetxController {
onLogin() async {
if (!userLogin.value) {
Get.toNamed(
'/webview',
parameters: {
'url': 'https://passport.bilibili.com/h5-app/passport/login',
'type': 'login',
'pageTitle': '登录bilibili',
},
);
// Get.toNamed('/loginPage');
// Get.toNamed(
// '/webview',
// parameters: {
// 'url': 'https://passport.bilibili.com/h5-app/passport/login',
// 'type': 'login',
// 'pageTitle': '登录bilibili',
// },
// );
Get.toNamed('/loginPage', preventDuplicates: false);
} else {
int mid = userInfo.value.mid!;
String face = userInfo.value.face!;
Get.toNamed(
'/member?mid=$mid',
arguments: {'face': face},
);
Get.toNamed('/member?mid=$mid',
arguments: {'face': face}, preventDuplicates: false);
}
}

View File

@@ -7,6 +7,7 @@ import 'package:PiliPalaX/models/common/theme_type.dart';
import 'package:PiliPalaX/utils/feed_back.dart';
import 'package:PiliPalaX/utils/login.dart';
import 'package:PiliPalaX/utils/storage.dart';
import 'package:webview_flutter/webview_flutter.dart';
import '../../models/common/dynamic_badge_mode.dart';
import '../../models/common/nav_bar_config.dart';
import '../main/index.dart';
@@ -32,7 +33,8 @@ class SettingController extends GetxController {
super.onInit();
userInfo = userInfoCache.get('userInfoCache');
userLogin.value = userInfo != null;
hiddenSettingUnlocked.value = setting.get(SettingBoxKey.hiddenSettingUnlocked, defaultValue: false);
hiddenSettingUnlocked.value =
setting.get(SettingBoxKey.hiddenSettingUnlocked, defaultValue: false);
feedBackEnable.value =
setting.get(SettingBoxKey.feedBackEnable, defaultValue: false);
toastOpacity.value =
@@ -65,12 +67,23 @@ class SettingController extends GetxController {
// 清空cookie
await Request.cookieManager.cookieJar.deleteAll();
Request.dio.options.headers['cookie'] = '';
// 清空本地存储的用户标识
userInfoCache.put('userInfoCache', null);
localCache
.put(LocalCacheKey.accessKey, {'mid': -1, 'value': ''});
localCache.put(LocalCacheKey.accessKey,
{'mid': -1, 'value': '', 'refresh': ''});
try {
final WebViewController controller = WebViewController();
controller.clearCache();
controller.clearLocalStorage();
WebViewCookieManager().clearCookies();
} catch (e) {
print(e);
}
userLogin.value = false;
if (Get.isRegistered<MainController>()) {
MainController mainController = Get.find<MainController>();
mainController.userLogin.value = false;
}
await LoginUtils.refreshLoginStatus(false);
Get.back();
},
@@ -107,8 +120,7 @@ class SettingController extends GetxController {
dynamicBadgeType.value = result;
setting.put(SettingBoxKey.dynamicBadgeMode, result.code);
MainController mainController = Get.put(MainController());
mainController.dynamicBadgeType =
DynamicBadgeMode.values[result.code];
mainController.dynamicBadgeType = DynamicBadgeMode.values[result.code];
if (mainController.dynamicBadgeType != DynamicBadgeMode.hidden) {
mainController.getUnreadDynamic();
}

View File

@@ -1,4 +1,3 @@
import 'package:PiliPalaX/utils/cookie.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@@ -68,42 +67,26 @@ class _PrivacySettingState extends State<PrivacySetting> {
subtitle: Text('已拉黑用户', style: subTitleStyle),
leading: const Icon(Icons.block),
),
ListTile(
onTap: () async {
if (!userLogin) {
SmartDialog.showToast('请先登录');
return;
}
var res = await MemberHttp.cookieToKey();
if (res['status']) {
SmartDialog.showToast(res['msg']);
} else {
SmartDialog.showToast("刷新失败:${res['msg']}");
}
},
dense: false,
title: Text('刷新access_key', style: titleStyle),
leading: const Icon(Icons.perm_device_info_outlined),
subtitle: Text(
'用于app端推荐接口的用户凭证。刷新有小概率导致其他设备下线。若app端未推荐个性化内容可尝试刷新或清除本app数据后重新登录',
style: subTitleStyle),
),
if (hiddenSettingUnlocked)
ListTile(
title: Text(
'导入/导出cookie',
style: titleStyle,
),
subtitle: Text(
'cookie代表您的登录状态仅供高级用户使用',
style: subTitleStyle,
),
leading: const Icon(Icons.cookie_outlined),
dense: false,
onTap: () {
import_export_cookies(titleStyle, subTitleStyle);
},
),
// ListTile(
// onTap: () async {
// if (!userLogin) {
// SmartDialog.showToast('请先登录');
// return;
// }
// var res = await MemberHttp.cookieToKey();
// if (res['status']) {
// SmartDialog.showToast(res['msg']);
// } else {
// SmartDialog.showToast("刷新失败:${res['msg']}");
// }
// },
// dense: false,
// title: Text('刷新access_key', style: titleStyle),
// leading: const Icon(Icons.perm_device_info_outlined),
// subtitle: Text(
// '用于app端推荐接口的用户凭证。若app端未推荐个性化内容可尝试刷新或清除本app数据后重新登录',
// style: subTitleStyle),
// ),
ListTile(
onTap: () {
MineController.onChangeAnonymity(context);
@@ -150,147 +133,4 @@ class _PrivacySettingState extends State<PrivacySetting> {
);
}
void import_export_cookies(TextStyle titleStyle, TextStyle subTitleStyle) {
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
title: const Text('导入/导出cookie', style: TextStyle(color: Colors.red)),
children: [
ListTile(
title: Text(
'导出cookie至剪贴板',
style: titleStyle.copyWith(color: Colors.red),
),
leading: const Icon(
Icons.warning_amber,
color: Colors.red,
),
subtitle: Text(
'泄露账号cookie等同于绕过账号密码与验证码直接登录可导致隐私泄露、风控、毁号、盗号等各类问题。\n'
'你应妥善保管该cookie且仅供自己使用。你承诺不会利用本服务进行任何违法或不当的活动。你承诺对所进行的一切活动'
'(包括但不限于网上点击同意或提交各类规则协议或购买服务、分享资讯或图片等)负全部责任。\n'
'你承诺、理解、同意并确认,在你的账户遭到未获授权的使用,或者发生其他任何安全问题时,'
'作者不对上述情形产生的任何直接或间接的遗失或损害承担责任。',
style: subTitleStyle.copyWith(color: Colors.redAccent),
),
dense: false,
onTap: () async {
Navigator.of(context).pop();
if (!userLogin) {
SmartDialog.showToast('请先登录');
return;
}
final String cookie = await CookieTool.exportCookie();
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('导出cookie危险',
style: TextStyle(color: Colors.red)),
content: Text(cookie),
actions: [
TextButton(
onPressed: () async {
Navigator.of(context).pop();
await Clipboard.setData(
ClipboardData(text: cookie));
},
child: const Text('复制(危险)',
style: TextStyle(color: Colors.red)),
),
TextButton(
onPressed: () async {
Navigator.of(context).pop();
},
child: const Text('取消'),
),
],
);
},
);
}),
ListTile(
title: Text(
'从剪贴板导入cookie',
style: titleStyle,
),
leading: const Icon(
Icons.warning_amber,
color: Colors.red,
),
subtitle: Text(
'导入将覆盖当前登录状态,你应自行对利用服务从事的所有行为及结果承担责任,请慎用',
style: subTitleStyle,
),
dense: false,
onTap: () async {
ClipboardData? data = await Clipboard.getData('text/plain');
if (data == null || data.text == null || data.text == '') {
SmartDialog.showToast('未检测到剪贴板内容');
return;
}
if (!context.mounted) return;
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('导入剪贴板中的cookie'),
content: Text(data.text!),
actions: [
TextButton(
onPressed: () async {
Get.back();
},
child: const Text('取消'),
),
TextButton(
onPressed: () async {
Get.back();
final String cookie = data.text!;
try {
await CookieTool.importCookie(cookie);
await SmartDialog.showToast('已导入');
await CookieTool.onSet();
final result = await UserHttp.userInfo();
if (result['status'] &&
result['data'].isLogin) {
SmartDialog.showToast('登录成功,当前采用「'
'${GStorage.setting.get(SettingBoxKey.defaultRcmdType, defaultValue: 'web')}'
'端」推荐');
Box userInfoCache = GStorage.userInfo;
await userInfoCache.put(
'userInfoCache', result['data']);
final HomeController homeCtr =
Get.find<HomeController>();
homeCtr.updateLoginStatus(true);
homeCtr.userFace.value = result['data'].face;
final MediaController mediaCtr =
Get.find<MediaController>();
mediaCtr.mid = result['data'].mid;
await LoginUtils.refreshLoginStatus(true);
Get.back();
} else {
// 获取用户信息失败
SmartDialog.showNotify(
msg:
'登录失败请检查cookie是否正确${result['message']}',
notifyType: NotifyType.warning);
}
} catch (e) {
SmartDialog.showToast('导入失败:$e');
}
},
child: const Text('确认'),
),
],
);
},
);
}),
],
);
},
);
}
}

View File

@@ -1,25 +1,16 @@
// ignore_for_file: avoid_print
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:PiliPalaX/http/init.dart';
import 'package:PiliPalaX/http/user.dart';
import 'package:PiliPalaX/pages/home/index.dart';
import 'package:PiliPalaX/pages/media/index.dart';
import 'package:PiliPalaX/utils/cookie.dart';
import 'package:PiliPalaX/utils/event_bus.dart';
import 'package:PiliPalaX/utils/id_utils.dart';
import 'package:PiliPalaX/utils/login.dart';
import 'package:PiliPalaX/utils/storage.dart';
import 'package:webview_flutter/webview_flutter.dart';
class WebviewController extends GetxController {
String url = '';
RxString type = ''.obs;
String pageTitle = '';
String uaType = '';
final WebViewController controller = WebViewController();
RxInt loadProgress = 0.obs;
RxBool loadShow = true.obs;
@@ -31,13 +22,9 @@ class WebviewController extends GetxController {
url = Get.parameters['url']!;
type.value = Get.parameters['type']!;
pageTitle = Get.parameters['pageTitle']!;
uaType = Get.parameters['uaType'] ?? 'mob';
if (type.value == 'login') {
controller.clearCache();
controller.clearLocalStorage();
WebViewCookieManager().clearCookies();
}
webviewInit();
webviewInit(uaType: uaType);
}
webviewInit({String uaType = 'mob'}) {
@@ -85,13 +72,7 @@ class WebviewController extends GetxController {
// 加载完成
onUrlChange: (UrlChange urlChange) async {
loadShow.value = false;
String url = urlChange.url ?? '';
if (type.value == 'login' &&
(url.startsWith(
'https://passport.bilibili.com/web/sso/exchange_cookie') ||
url.startsWith('https://m.bilibili.com/'))) {
confirmLogin(url);
}
// String url = urlChange.url ?? '';
},
onWebResourceError: (WebResourceError error) {},
onNavigationRequest: (NavigationRequest request) {
@@ -112,52 +93,4 @@ class WebviewController extends GetxController {
..loadRequest(Uri.parse(url));
}
confirmLogin(url) async {
var content = '';
if (url != null) {
content = '${content + url}; \n';
}
try {
await CookieTool.onSet();
final result = await UserHttp.userInfo();
if (result['status'] && result['data'].isLogin) {
SmartDialog.showToast('登录成功,当前采用「'
'${GStorage.setting.get(SettingBoxKey.defaultRcmdType, defaultValue: 'web')}'
'端」推荐');
try {
Box userInfoCache = GStorage.userInfo;
await userInfoCache.put('userInfoCache', result['data']);
final HomeController homeCtr = Get.find<HomeController>();
homeCtr.updateLoginStatus(true);
homeCtr.userFace.value = result['data'].face;
final MediaController mediaCtr = Get.find<MediaController>();
mediaCtr.mid = result['data'].mid;
await LoginUtils.refreshLoginStatus(true);
} catch (err) {
SmartDialog.show(builder: (BuildContext context) {
return AlertDialog(
title: const Text('登录遇到问题'),
content: Text(err.toString()),
actions: [
TextButton(
onPressed: () => controller.reload(),
child: const Text('确认'),
)
],
);
});
}
Get.back();
} else {
// 获取用户信息失败
SmartDialog.showToast(result['msg']);
Clipboard.setData(ClipboardData(text: result['msg']));
}
} catch (e) {
SmartDialog.showNotify(msg: e.toString(), notifyType: NotifyType.warning);
content = content + e.toString();
Clipboard.setData(ClipboardData(text: content));
}
}
}

View File

@@ -36,25 +36,14 @@ class _WebviewPageState extends State<WebviewPage> {
icon: Icon(Icons.refresh_outlined,
color: Theme.of(context).colorScheme.primary),
),
if (_webviewController.type.value != 'login')
IconButton(
tooltip: '用外部浏览器打开',
onPressed: () {
launchUrl(Uri.parse(_webviewController.url));
},
icon: Icon(Icons.open_in_browser_outlined,
color: Theme.of(context).colorScheme.primary),
),
if (_webviewController.type.value == 'login') ...<Widget>[
TextButton(
onPressed: () => _webviewController.confirmLogin(null),
child: const Text('刷新登录态'),
),
TextButton(
child: const Text('电脑版'),
onPressed: () => _webviewController.webviewInit(uaType: 'pc'),
)
],
IconButton(
tooltip: '用外部浏览器打开',
onPressed: () {
launchUrl(Uri.parse(_webviewController.url));
},
icon: Icon(Icons.open_in_browser_outlined,
color: Theme.of(context).colorScheme.primary),
),
const SizedBox(width: 12)
],
),

View File

@@ -1,49 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:PiliPalaX/http/constants.dart';
import 'package:PiliPalaX/http/init.dart';
import 'package:webview_cookie_manager/webview_cookie_manager.dart';
class CookieTool {
static exportCookie() async {
Map<String, String> allCookies = {};
List<String> Urls = [HttpString.baseUrl, HttpString.apiBaseUrl, HttpString.tUrl];
for (var url in Urls) {
allCookies[url] = await WebviewCookieManager().getCookies(url)
.then((cookies) => cookies.map((cookie) => '${cookie.name}=${cookie.value}').join('; '));
}
return jsonEncode(allCookies);
}
static importCookie(String cookie) async {
var allCookies = jsonDecode(cookie);
for (var url in allCookies.keys) {
List<String> cookiesStringList = allCookies[url]!.split('; ');
List<Cookie> cookies = [];
for (var c in cookiesStringList) {
List<String> kv = c.split('=');
cookies.add(Cookie(kv[0], kv[1]));
}
await Request.cookieManager.cookieJar.saveFromResponse(Uri.parse(url), cookies);
if (url == HttpString.baseUrl) {
Request.dio.options.headers['cookie'] = allCookies[url];
}
}
}
static onSet() async {
var cookies = await WebviewCookieManager().getCookies(HttpString.baseUrl);
await Request.cookieManager.cookieJar
.saveFromResponse(Uri.parse(HttpString.baseUrl), cookies);
var cookieString =
cookies.map((cookie) => '${cookie.name}=${cookie.value}').join('; ');
Request.dio.options.headers['cookie'] = cookieString;
cookies = await WebviewCookieManager().getCookies(HttpString.apiBaseUrl);
await Request.cookieManager.cookieJar
.saveFromResponse(Uri.parse(HttpString.apiBaseUrl), cookies);
cookies = await WebviewCookieManager().getCookies(HttpString.tUrl);
await Request.cookieManager.cookieJar
.saveFromResponse(Uri.parse(HttpString.tUrl), cookies);
}
}