Merge branch 'design' into alpha

This commit is contained in:
guozhigq
2023-11-11 23:40:11 +08:00
17 changed files with 964 additions and 20 deletions

View File

@ -12,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):
@ -56,6 +60,7 @@ DEPENDENCIES:
- 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`)
@ -77,6 +82,7 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- FMDB
- GT3Captcha-iOS
- ReachabilitySwift
EXTERNAL SOURCES:
@ -90,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,6 +140,8 @@ SPEC CHECKSUMS:
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

View File

@ -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 */

View File

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

View File

@ -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';

177
lib/http/login.dart Normal file
View File

@ -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<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() {
var mac = <String>[];
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<String, dynamic> 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);
}
}

View File

@ -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<String, dynamic> 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<String, dynamic> json) {
challenge = json["challenge"];
gt = json["gt"];
}
}
class Tencent {
Tencent({this.appid});
String? appid;
Tencent.fromJson(Map<String, dynamic> json) {
appid = json["appid"];
}
}

View File

@ -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<FormState>();
final GlobalKey passwordFormKey = GlobalKey<FormState>();
final GlobalKey msgCodeFormKey = GlobalKey<FormState>();
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<String, dynamic> message) async {
SmartDialog.dismiss();
}, 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"];
// 处理验证中返回的错误 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 {}
}
}

View File

@ -0,0 +1,4 @@
library login;
export './controller.dart';
export 'view.dart';

362
lib/pages/login/view.dart Normal file
View File

@ -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<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
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('确认登录'),
)
],
),
],
),
),
),
),
],
),
);
}
}

View File

@ -41,6 +41,7 @@ class MineController extends GetxController {
'pageTitle': '登录bilibili',
},
);
// Get.toNamed('/loginPage');
} else {
int mid = userInfo.value.mid!;
String face = userInfo.value.face!;

View File

@ -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 = [];

View File

@ -299,20 +299,24 @@ class _SearchPageState extends State<SearchPage> 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),
)
],
)),
],
),
),

View File

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

View File

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

View File

@ -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 = <String>[];
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()}';
}
}

View File

@ -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:
@ -401,6 +409,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:
@ -613,6 +629,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:
@ -1420,7 +1444,7 @@ packages:
source: hosted
version: "3.0.7"
uuid:
dependency: transitive
dependency: "direct main"
description:
name: uuid
sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313"

View File

@ -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.
@ -128,6 +129,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: