From d105718fbf1cd9bcec697d7eba12d16205adde1d Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sat, 11 Nov 2023 23:18:19 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20app=E7=AB=AF=E7=99=BB=E5=BD=95-?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Podfile.lock | 16 +- ios/Runner.xcodeproj/project.pbxproj | 18 ++ lib/http/api.dart | 36 +++ lib/http/init.dart | 8 +- lib/http/login.dart | 177 +++++++++++++ lib/models/login/index.dart | 49 ++++ lib/pages/login/controller.dart | 204 +++++++++++++++ lib/pages/login/index.dart | 4 + lib/pages/login/view.dart | 362 +++++++++++++++++++++++++++ lib/pages/mine/controller.dart | 1 + lib/router/app_pages.dart | 3 + lib/utils/login.dart | 30 +++ pubspec.lock | 26 +- pubspec.yaml | 4 + 14 files changed, 928 insertions(+), 10 deletions(-) create mode 100644 lib/http/login.dart create mode 100644 lib/models/login/index.dart create mode 100644 lib/pages/login/controller.dart create mode 100644 lib/pages/login/index.dart create mode 100644 lib/pages/login/view.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 3e0ed4ca..9d796293 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,8 +1,6 @@ PODS: - appscheme (1.0.4): - Flutter - - auto_orientation (0.0.1): - - Flutter - connectivity_plus (0.0.1): - Flutter - ReachabilitySwift @@ -14,6 +12,10 @@ PODS: - FMDB (2.7.5): - FMDB/standard (= 2.7.5) - FMDB/standard (2.7.5) + - gt3_flutter_plugin (0.0.8): + - Flutter + - GT3Captcha-iOS + - GT3Captcha-iOS (0.15.8.3) - media_kit_libs_ios_video (1.0.4): - Flutter - media_kit_native_event_loop (1.0.0): @@ -54,11 +56,11 @@ PODS: DEPENDENCIES: - appscheme (from `.symlinks/plugins/appscheme/ios`) - - auto_orientation (from `.symlinks/plugins/auto_orientation/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - Flutter (from `Flutter`) - flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`) + - gt3_flutter_plugin (from `.symlinks/plugins/gt3_flutter_plugin/ios`) - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) - media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`) - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) @@ -80,13 +82,12 @@ DEPENDENCIES: SPEC REPOS: trunk: - FMDB + - GT3Captcha-iOS - ReachabilitySwift EXTERNAL SOURCES: appscheme: :path: ".symlinks/plugins/appscheme/ios" - auto_orientation: - :path: ".symlinks/plugins/auto_orientation/ios" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" device_info_plus: @@ -95,6 +96,8 @@ EXTERNAL SOURCES: :path: Flutter flutter_volume_controller: :path: ".symlinks/plugins/flutter_volume_controller/ios" + gt3_flutter_plugin: + :path: ".symlinks/plugins/gt3_flutter_plugin/ios" media_kit_libs_ios_video: :path: ".symlinks/plugins/media_kit_libs_ios_video/ios" media_kit_native_event_loop: @@ -132,12 +135,13 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: appscheme: b1c3f8862331cb20430cf9e0e4af85dbc1572ad8 - auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + gt3_flutter_plugin: bfa1f26e9a09dc00401514be5ed437f964cabf23 + GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6 media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 6f46bb30..eeb03bef 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -140,6 +140,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 5A372F23F3CF0118D6526BAC /* [CP] Embed Pods Frameworks */, + B78851E7B29A4C3961AC483C /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -268,6 +269,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + B78851E7B29A4C3961AC483C /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/lib/http/api.dart b/lib/http/api.dart index f2f06007..042d8e11 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -336,4 +336,40 @@ class Api { /// w_rid=1607c6c5a4a35a1297e31992220900ae& /// wts=1697033079 static const String aiConclusion = '/x/web-interface/view/conclusion/get'; + + // captcha验证码 + static const String getCaptcha = + 'https://passport.bilibili.com/x/passport-login/captcha?source=main_web'; + + // web端短信验证码 + static const String smsCode = + 'https://passport.bilibili.com/x/passport-login/web/sms/send'; + + // web端验证码登录 + + // web端密码登录 + + // app端短信验证码 + static const String appSmsCode = + 'https://passport.bilibili.com/x/passport-login/sms/send'; + + // app端验证码登录 + + // 获取短信验证码 + // static const String appSafeSmsCode = + // 'https://passport.bilibili.com/x/safecenter/common/sms/send'; + + /// app端密码登录 + /// username + /// password + /// key + /// rhash + static const String loginInByPwdApi = + 'https://passport.bilibili.com/x/passport-login/oauth2/login'; + + /// 密码加密密钥 + /// disable_rcmd + /// local_id + static const getWebKey = + 'https://passport.bilibili.com/x/passport-login/web/key'; } diff --git a/lib/http/init.dart b/lib/http/init.dart index e2b3cd1f..6a60dca0 100644 --- a/lib/http/init.dart +++ b/lib/http/init.dart @@ -47,8 +47,8 @@ class Request { log("setCookie, ${e.toString()}"); } } - setOptionsHeaders(userInfo); } + setOptionsHeaders(userInfo, userInfo != null && userInfo.mid != null); if (cookie.isEmpty) { try { @@ -73,8 +73,10 @@ class Request { return token; } - static setOptionsHeaders(userInfo) { - dio.options.headers['x-bili-mid'] = userInfo.mid.toString(); + static setOptionsHeaders(userInfo, status) { + if (status) { + dio.options.headers['x-bili-mid'] = userInfo.mid.toString(); + } dio.options.headers['env'] = 'prod'; dio.options.headers['app-key'] = 'android64'; dio.options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH'; diff --git a/lib/http/login.dart b/lib/http/login.dart new file mode 100644 index 00000000..8d2a254e --- /dev/null +++ b/lib/http/login.dart @@ -0,0 +1,177 @@ +import 'dart:convert'; +import 'dart:math'; +import 'package:crypto/crypto.dart'; + +import 'package:dio/dio.dart'; +import 'package:encrypt/encrypt.dart'; +import 'package:pilipala/http/index.dart'; +import 'package:pilipala/models/login/index.dart'; +import 'package:pilipala/utils/login.dart'; +import 'package:uuid/uuid.dart'; + +class LoginHttp { + static Future queryCaptcha() async { + var res = await Request().get(Api.getCaptcha); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': CaptchaDataModel.fromJson(res.data['data']), + }; + } else { + return {'status': false, 'data': res.message}; + } + } + + static Future sendSmsCode({ + int? cid, + required int tel, + required String token, + required String challenge, + required String validate, + required String seccode, + }) async { + var res = await Request().post( + Api.appSmsCode, + data: { + 'cid': cid, + 'tel': tel, + "source": "main_web", + 'token': token, + 'challenge': challenge, + 'validate': validate, + 'seccode': seccode, + }, + options: Options( + contentType: Headers.formUrlEncodedContentType, + // headers: {'user-agent': ApiConstants.userAgent} + ), + ); + print(res); + } + + // 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); + } + + // 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 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() { + var mac = []; + var random = Random(); + + for (var i = 0; i < 6; i++) { + var min = 0; + var max = 0xff; + var num = (random.nextInt(max - min + 1) + min).toRadixString(16); + mac.add(num); + } + + var md5Str = md5.convert(utf8.encode(mac.join(':'))).toString(); + var md5Arr = md5Str.split(''); + 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']}; + } + } + + // app端密码登录 + static Future loginInByMobPwd({ + required String tel, + required String password, + required String key, + required String rhash, + }) async { + dynamic publicKey = RSAKeyParser().parse(key); + String passwordEncryptyed = + Encrypter(RSA(publicKey: publicKey)).encrypt(rhash + password).base64; + Map data = { + 'username': tel, + 'password': passwordEncryptyed, + 'local_id': LoginUtils.generateBuvid(), + 'disable_rcmd': "0", + }; + var res = await Request().post( + Api.loginInByPwdApi, + data: data, + options: Options( + contentType: Headers.formUrlEncodedContentType, + ), + ); + print(res); + } +} diff --git a/lib/models/login/index.dart b/lib/models/login/index.dart new file mode 100644 index 00000000..a4f2e3c0 --- /dev/null +++ b/lib/models/login/index.dart @@ -0,0 +1,49 @@ +class CaptchaDataModel { + CaptchaDataModel({ + this.type, + this.token, + this.geetest, + this.tencent, + this.validate, + this.seccode, + }); + + String? type; + String? token; + GeetestData? geetest; + Tencent? tencent; + String? validate; + String? seccode; + + CaptchaDataModel.fromJson(Map json) { + type = json["type"]; + token = json["token"]; + geetest = + json["geetest"] != null ? GeetestData.fromJson(json["geetest"]) : null; + tencent = + json["tencent"] != null ? Tencent.fromJson(json["tencent"]) : null; + } +} + +class GeetestData { + GeetestData({ + this.challenge, + this.gt, + }); + + String? challenge; + String? gt; + + GeetestData.fromJson(Map json) { + challenge = json["challenge"]; + gt = json["gt"]; + } +} + +class Tencent { + Tencent({this.appid}); + String? appid; + Tencent.fromJson(Map json) { + appid = json["appid"]; + } +} diff --git a/lib/pages/login/controller.dart b/lib/pages/login/controller.dart new file mode 100644 index 00000000..c002fdf9 --- /dev/null +++ b/lib/pages/login/controller.dart @@ -0,0 +1,204 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/http/login.dart'; +import 'package:gt3_flutter_plugin/gt3_flutter_plugin.dart'; +import 'package:pilipala/models/login/index.dart'; + +class LoginPageController extends GetxController { + final GlobalKey mobFormKey = GlobalKey(); + final GlobalKey passwordFormKey = GlobalKey(); + final GlobalKey msgCodeFormKey = GlobalKey(); + + final TextEditingController mobTextController = TextEditingController(); + final TextEditingController passwordTextController = TextEditingController(); + final TextEditingController msgCodeTextController = TextEditingController(); + + final FocusNode mobTextFieldNode = FocusNode(); + final FocusNode passwordTextFieldNode = FocusNode(); + final FocusNode msgCodeTextFieldNode = FocusNode(); + + final PageController pageViewController = PageController(); + + RxInt currentIndex = 0.obs; + + final Gt3FlutterPlugin captcha = Gt3FlutterPlugin(); + + // 默认密码登录 + RxInt loginType = 0.obs; + + // 监听pageView切换 + void onPageChange(int index) { + currentIndex.value = index; + } + + // 输入手机号 下一页 + void nextStep() async { + if ((mobFormKey.currentState as FormState).validate()) { + await pageViewController.animateToPage( + 1, + duration: const Duration(microseconds: 3000), + curve: Curves.easeInOut, + ); + passwordTextFieldNode.requestFocus(); + } + } + + // 上一页 + void previousPage() async { + passwordTextFieldNode.unfocus(); + await Future.delayed(const Duration(milliseconds: 200)); + pageViewController.animateToPage( + 0, + duration: const Duration(microseconds: 300), + curve: Curves.easeInOut, + ); + } + + // 切换登录方式 + 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, + ); + } else { + SmartDialog.showToast(webKeyRes['msg']); + } + } + } + + // 验证码登录 + 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 getCaptcha(oncall) async { + SmartDialog.showLoading(msg: '请求中...'); + var result = await LoginHttp.queryCaptcha(); + if (result['status']) { + CaptchaDataModel captchaData = result['data']; + var registerData = Gt3RegisterData( + challenge: captchaData.geetest!.challenge, + gt: captchaData.geetest!.gt!, + success: true, + ); + captcha.addEventHandler(onShow: (Map message) async { + SmartDialog.dismiss(); + }, onClose: (Map message) async { + SmartDialog.showToast('关闭验证'); + }, onResult: (Map 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 message) async { + 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 + } + } + }); + captcha.startCaptcha(registerData); + } else {} + } +} diff --git a/lib/pages/login/index.dart b/lib/pages/login/index.dart new file mode 100644 index 00000000..cdc05abd --- /dev/null +++ b/lib/pages/login/index.dart @@ -0,0 +1,4 @@ +library login; + +export './controller.dart'; +export 'view.dart'; diff --git a/lib/pages/login/view.dart b/lib/pages/login/view.dart new file mode 100644 index 00000000..6521e9d9 --- /dev/null +++ b/lib/pages/login/view.dart @@ -0,0 +1,362 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import 'controller.dart'; + +class LoginPage extends StatefulWidget { + const LoginPage({super.key}); + + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + final LoginPageController _loginPageCtr = Get.put(LoginPageController()); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: Obx( + () => _loginPageCtr.currentIndex.value == 0 + ? IconButton( + onPressed: () async { + _loginPageCtr.mobTextFieldNode.unfocus(); + await Future.delayed(const Duration(milliseconds: 200)); + Get.back(); + }, + icon: const Icon(Icons.close_outlined), + ) + : IconButton( + 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, + ), + 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), + ), + Row( + children: [ + Text( + '请使用您的 BiliBili 账号登录。', + style: Theme.of(context).textTheme.titleSmall!, + ), + GestureDetector( + onTap: () {}, + child: const Icon(Icons.info_outline, size: 16), + ) + ], + ), + 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('下一步'), + ) + ], + ), + ], + ), + ), + ), + 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( + 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( + 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('确认登录'), + ) + ], + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/mine/controller.dart b/lib/pages/mine/controller.dart index 772ba06a..2b53850b 100644 --- a/lib/pages/mine/controller.dart +++ b/lib/pages/mine/controller.dart @@ -41,6 +41,7 @@ class MineController extends GetxController { 'pageTitle': '登录bilibili', }, ); + // Get.toNamed('/loginPage'); } else { int mid = userInfo.value.mid!; String face = userInfo.value.face!; diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 544217f9..6accf4f1 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -19,6 +19,7 @@ import 'package:pilipala/pages/hot/index.dart'; import 'package:pilipala/pages/html/index.dart'; import 'package:pilipala/pages/later/index.dart'; import 'package:pilipala/pages/liveRoom/view.dart'; +import 'package:pilipala/pages/login/index.dart'; import 'package:pilipala/pages/member/index.dart'; import 'package:pilipala/pages/member_search/index.dart'; import 'package:pilipala/pages/preview/index.dart'; @@ -122,6 +123,8 @@ class Routes { CustomGetPage(name: '/playSpeedSet', page: () => const PlaySpeedPage()), // 收藏搜索 CustomGetPage(name: '/favSearch', page: () => const FavSearchPage()), + // 登录页面 + CustomGetPage(name: '/loginPage', page: () => const LoginPage()), ]; } diff --git a/lib/utils/login.dart b/lib/utils/login.dart index 54c03775..59c53027 100644 --- a/lib/utils/login.dart +++ b/lib/utils/login.dart @@ -1,9 +1,14 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:crypto/crypto.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:pilipala/pages/dynamics/index.dart'; import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/pages/mine/index.dart'; +import 'package:uuid/uuid.dart'; class LoginUtils { static Future refreshLoginStatus(bool status) async { @@ -27,4 +32,29 @@ class LoginUtils { SmartDialog.showToast('refreshLoginStatus error: ${err.toString()}'); } } + + static String buvid() { + var mac = []; + var random = Random(); + + for (var i = 0; i < 6; i++) { + var min = 0; + var max = 0xff; + var num = (random.nextInt(max - min + 1) + min).toRadixString(16); + mac.add(num); + } + + var md5Str = md5.convert(utf8.encode(mac.join(':'))).toString(); + var md5Arr = md5Str.split(''); + return 'XY${md5Arr[2]}${md5Arr[12]}${md5Arr[22]}$md5Str'; + } + + static String getUUID() { + return const Uuid().v4().replaceAll('-', ''); + } + + static String generateBuvid() { + String uuid = getUUID() + getUUID(); + return 'XY${uuid.substring(0, 35).toUpperCase()}'; + } } diff --git a/pubspec.lock b/pubspec.lock index 4cbbd2e9..a70c3553 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.2" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "21afe4333076c02877d14f4a89df111e658a6d466cbfc802eb705eb91bd5adfd" + url: "https://pub.dev" + source: hosted + version: "1.5.0" async: dependency: transitive description: @@ -369,6 +377,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + encrypt: + dependency: "direct main" + description: + name: encrypt + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + url: "https://pub.dev" + source: hosted + version: "5.0.3" extended_image: dependency: "direct main" description: @@ -581,6 +597,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + gt3_flutter_plugin: + dependency: "direct main" + description: + name: gt3_flutter_plugin + sha256: f12bff2bfbcf27467833f8d564dcc24ee2f1b3254a7c7cf5eb2c4590baf11cc1 + url: "https://pub.dev" + source: hosted + version: "0.0.8" hive: dependency: "direct main" description: @@ -1388,7 +1412,7 @@ packages: source: hosted version: "3.0.7" uuid: - dependency: transitive + dependency: "direct main" description: name: uuid sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" diff --git a/pubspec.yaml b/pubspec.yaml index a676942b..090e8a38 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -82,6 +82,7 @@ dependencies: custom_sliding_segmented_control: ^1.7.5 # 加密 crypto: ^3.0.3 + encrypt: ^5.0.3 # 视频播放器 media_kit: ^1.1.10 # Primary package. @@ -124,6 +125,9 @@ dependencies: html: ^0.15.4 # html渲染 flutter_html: ^3.0.0-beta.2 + # 极验 + gt3_flutter_plugin: ^0.0.8 + uuid: ^3.0.7 dev_dependencies: From 96523a99ce0065af4d292b8b944b075b4d3f7de2 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sat, 11 Nov 2023 23:39:54 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E9=95=BF=E6=8C=89=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E6=90=9C=E7=B4=A2=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/search/controller.dart | 7 +++++ lib/pages/search/view.dart | 32 +++++++++++++---------- lib/pages/search/widgets/search_text.dart | 13 +++++++-- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/lib/pages/search/controller.dart b/lib/pages/search/controller.dart index b95b048b..59d51a41 100644 --- a/lib/pages/search/controller.dart +++ b/lib/pages/search/controller.dart @@ -117,6 +117,13 @@ class SSearchController extends GetxController { submit(); } + onLongSelect(word) { + int index = historyList.indexOf(word); + historyList.value = historyList.removeAt(index); + historyList.refresh(); + histiryWord.put('cacheList', historyList); + } + onClearHis() { historyList.value = []; historyCacheList = []; diff --git a/lib/pages/search/view.dart b/lib/pages/search/view.dart index 81f56ce0..0ec910f1 100644 --- a/lib/pages/search/view.dart +++ b/lib/pages/search/view.dart @@ -299,20 +299,24 @@ class _SearchPageState extends State with RouteAware { ), ), // if (_searchController.historyList.isNotEmpty) - Wrap( - spacing: 8, - runSpacing: 8, - direction: Axis.horizontal, - textDirection: TextDirection.ltr, - children: [ - for (int i = 0; i < _searchController.historyList.length; i++) - SearchText( - searchText: _searchController.historyList[i], - searchTextIdx: i, - onSelect: (value) => _searchController.onSelect(value), - ) - ], - ), + Obx(() => Wrap( + spacing: 8, + runSpacing: 8, + direction: Axis.horizontal, + textDirection: TextDirection.ltr, + children: [ + for (int i = 0; + i < _searchController.historyList.length; + i++) + SearchText( + searchText: _searchController.historyList[i], + searchTextIdx: i, + onSelect: (value) => _searchController.onSelect(value), + onLongSelect: (value) => + _searchController.onLongSelect(value), + ) + ], + )), ], ), ), diff --git a/lib/pages/search/widgets/search_text.dart b/lib/pages/search/widgets/search_text.dart index 9f5f84c3..039a851b 100644 --- a/lib/pages/search/widgets/search_text.dart +++ b/lib/pages/search/widgets/search_text.dart @@ -4,8 +4,14 @@ class SearchText extends StatelessWidget { final String? searchText; final Function? onSelect; final int? searchTextIdx; - const SearchText( - {super.key, this.searchText, this.onSelect, this.searchTextIdx}); + final Function? onLongSelect; + const SearchText({ + super.key, + this.searchText, + this.onSelect, + this.searchTextIdx, + this.onLongSelect, + }); @override Widget build(BuildContext context) { @@ -18,6 +24,9 @@ class SearchText extends StatelessWidget { onTap: () { onSelect!(searchText); }, + onLongPress: () { + onLongSelect!(searchText); + }, borderRadius: BorderRadius.circular(6), child: Padding( padding: