Merge branch 'main' into feature-appScheme

This commit is contained in:
guozhigq
2024-06-19 23:40:08 +08:00
80 changed files with 3247 additions and 975 deletions

BIN
assets/images/coin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

1
assets/loading.json Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -23,7 +23,7 @@ PODS:
- FMDB (2.7.5): - FMDB (2.7.5):
- FMDB/standard (= 2.7.5) - FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5) - FMDB/standard (2.7.5)
- gt3_flutter_plugin (0.0.8): - gt3_flutter_plugin (0.0.9):
- Flutter - Flutter
- GT3Captcha-iOS - GT3Captcha-iOS
- GT3Captcha-iOS (0.15.8.3) - GT3Captcha-iOS (0.15.8.3)
@ -171,13 +171,13 @@ SPEC CHECKSUMS:
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529 flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
gt3_flutter_plugin: bfa1f26e9a09dc00401514be5ed437f964cabf23 gt3_flutter_plugin: 5bd2c08d3c19cbb6ee3b08f4358439e54c8ab2ee
GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6 GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
saver_gallery: 2b4e584106fde2407ab51560f3851564963e6b78 saver_gallery: 2b4e584106fde2407ab51560f3851564963e6b78
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625

View File

@ -124,7 +124,7 @@ class EpisodeBottomSheet {
}); });
return Container( return Container(
height: sheetHeight, height: sheetHeight,
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.surface,
child: Column( child: Column(
children: [ children: [
buildTitle(), buildTitle(),

View File

@ -13,8 +13,8 @@ class Skeleton extends StatelessWidget {
var shimmerGradient = LinearGradient( var shimmerGradient = LinearGradient(
colors: [ colors: [
Colors.transparent, Colors.transparent,
Theme.of(context).colorScheme.background.withAlpha(10), Theme.of(context).colorScheme.surface.withAlpha(10),
Theme.of(context).colorScheme.background.withAlpha(10), Theme.of(context).colorScheme.surface.withAlpha(10),
Colors.transparent, Colors.transparent,
], ],
stops: const [ stops: const [

View File

@ -14,7 +14,7 @@ class StatDanMu extends StatelessWidget {
Map<String, Color> colorObject = { Map<String, Color> colorObject = {
'white': Colors.white, 'white': Colors.white,
'gray': Theme.of(context).colorScheme.outline, 'gray': Theme.of(context).colorScheme.outline,
'black': Theme.of(context).colorScheme.onBackground.withOpacity(0.8), 'black': Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
}; };
Color color = colorObject[theme]!; Color color = colorObject[theme]!;
return Row( return Row(

View File

@ -14,7 +14,7 @@ class StatView extends StatelessWidget {
Map<String, Color> colorObject = { Map<String, Color> colorObject = {
'white': Colors.white, 'white': Colors.white,
'gray': Theme.of(context).colorScheme.outline, 'gray': Theme.of(context).colorScheme.outline,
'black': Theme.of(context).colorScheme.onBackground.withOpacity(0.8), 'black': Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
}; };
Color color = colorObject[theme]!; Color color = colorObject[theme]!;
return Row( return Row(

View File

@ -189,7 +189,7 @@ class Api {
'https://s.search.bilibili.com/main/suggest'; 'https://s.search.bilibili.com/main/suggest';
// 分类搜索 // 分类搜索
static const String searchByType = '/x/web-interface/search/type'; static const String searchByType = '/x/web-interface/wbi/search/type';
// 记录视频播放进度 // 记录视频播放进度
// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/report.md // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/report.md
@ -400,12 +400,24 @@ class Api {
'${HttpString.passBaseUrl}/x/passport-login/captcha?source=main_web'; '${HttpString.passBaseUrl}/x/passport-login/captcha?source=main_web';
// web端短信验证码 // web端短信验证码
static const String smsCode = static const String webSmsCode =
'${HttpString.passBaseUrl}/x/passport-login/web/sms/send'; '${HttpString.passBaseUrl}/x/passport-login/web/sms/send';
// web端验证码登录 // web端验证码登录
static const String webSmsLogin =
'${HttpString.passBaseUrl}/x/passport-login/web/login/sms';
// web端密码登录 // web端密码登录
static const String loginInByWebPwd =
'${HttpString.passBaseUrl}/x/passport-login/web/login';
// web端二维码
static const String qrCodeApi =
'${HttpString.passBaseUrl}/x/passport-login/web/qrcode/generate';
// 扫码登录
static const String loginInByQrcode =
'${HttpString.passBaseUrl}/x/passport-login/web/qrcode/poll';
// app端短信验证码 // app端短信验证码
static const String appSmsCode = static const String appSmsCode =
@ -523,4 +535,17 @@ class Api {
/// 搜索结果计数 /// 搜索结果计数
static const String searchCount = '/x/web-interface/wbi/search/all/v2'; static const String searchCount = '/x/web-interface/wbi/search/all/v2';
/// 关闭会话
static const String removeSession =
'${HttpString.tUrl}/session_svr/v1/session_svr/remove_session';
/// 消息未读数
static const String unread = '${HttpString.tUrl}/x/im/web/msgfeed/unread';
/// 回复我的
static const String messageReplyAPi = '/x/msgfeed/reply';
/// 收到的赞
static const String messageLikeAPi = '/x/msgfeed/like';
} }

View File

@ -3,6 +3,7 @@ import 'dart:math';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:encrypt/encrypt.dart'; import 'package:encrypt/encrypt.dart';
import 'package:pilipala/http/constants.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import '../models/login/index.dart'; import '../models/login/index.dart';
import '../utils/login.dart'; import '../utils/login.dart';
@ -21,32 +22,32 @@ class LoginHttp {
} }
} }
static Future sendSmsCode({ // static Future sendSmsCode({
int? cid, // int? cid,
required int tel, // required int tel,
required String token, // required String token,
required String challenge, // required String challenge,
required String validate, // required String validate,
required String seccode, // required String seccode,
}) async { // }) async {
var res = await Request().post( // var res = await Request().post(
Api.appSmsCode, // Api.appSmsCode,
data: { // data: {
'cid': cid, // 'cid': cid,
'tel': tel, // 'tel': tel,
"source": "main_web", // "source": "main_web",
'token': token, // 'token': token,
'challenge': challenge, // 'challenge': challenge,
'validate': validate, // 'validate': validate,
'seccode': seccode, // 'seccode': seccode,
}, // },
options: Options( // options: Options(
contentType: Headers.formUrlEncodedContentType, // contentType: Headers.formUrlEncodedContentType,
// headers: {'user-agent': ApiConstants.userAgent} // // headers: {'user-agent': ApiConstants.userAgent}
), // ),
); // );
print(res); // print(res);
} // }
// web端验证码 // web端验证码
static Future sendWebSmsCode({ static Future sendWebSmsCode({
@ -60,6 +61,7 @@ class LoginHttp {
Map data = { Map data = {
'cid': cid, 'cid': cid,
'tel': tel, 'tel': tel,
"source": "main_web",
'token': token, 'token': token,
'challenge': challenge, 'challenge': challenge,
'validate': validate, 'validate': validate,
@ -67,17 +69,56 @@ class LoginHttp {
}; };
FormData formData = FormData.fromMap({...data}); FormData formData = FormData.fromMap({...data});
var res = await Request().post( var res = await Request().post(
Api.smsCode, Api.webSmsCode,
data: formData, data: formData,
options: Options( options: Options(
contentType: Headers.formUrlEncodedContentType, contentType: Headers.formUrlEncodedContentType,
), ),
); );
print(res); if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
} }
// web端验证码登录 // web端验证码登录
static Future loginInByWebSmsCode() async {} static Future loginInByWebSmsCode({
int? cid,
required int tel,
required int code,
required String captchaKey,
}) async {
// webSmsLogin
Map data = {
"cid": cid,
"tel": tel,
"code": code,
"source": "main_mini",
"keep": 0,
"captcha_key": captchaKey,
"go_url": HttpString.baseUrl
};
FormData formData = FormData.fromMap({...data});
var res = await Request().post(
Api.webSmsLogin,
data: formData,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
// web端密码登录 // web端密码登录
static Future liginInByWebPwd() async {} static Future liginInByWebPwd() async {}
@ -173,4 +214,69 @@ class LoginHttp {
); );
print(res); print(res);
} }
// web端密码登录
static Future loginInByWebPwd({
required int username,
required String password,
required String token,
required String challenge,
required String validate,
required String seccode,
}) async {
Map data = {
'username': username,
'password': password,
'keep': 0,
'token': token,
'challenge': challenge,
'validate': validate,
'seccode': seccode,
'source': 'main-fe-header',
"go_url": HttpString.baseUrl
};
FormData formData = FormData.fromMap({...data});
var res = await Request().post(
Api.loginInByWebPwd,
data: formData,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
// web端登录二维码
static Future getWebQrcode() async {
var res = await Request().get(Api.qrCodeApi);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
// web端二维码轮询登录状态
static Future queryWebQrcodeStatus(String qrcodeKey) async {
var res = await Request()
.get(Api.loginInByQrcode, data: {'qrcode_key': qrcodeKey});
if (res.data['data']['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
} }

View File

@ -1,4 +1,8 @@
import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:dio/dio.dart';
import 'package:pilipala/models/msg/like.dart';
import 'package:pilipala/models/msg/reply.dart';
import '../models/msg/account.dart'; import '../models/msg/account.dart';
import '../models/msg/session.dart'; import '../models/msg/session.dart';
import '../utils/wbi_sign.dart'; import '../utils/wbi_sign.dart';
@ -122,68 +126,48 @@ class MsgHttp {
'data': res.data['data'], 'data': res.data['data'],
}; };
} else { } else {
return { return {'status': false, 'date': [], 'msg': res.data['message']};
'status': false,
'date': [],
'msg': "message: ${res.data['message']},"
" msg: ${res.data['msg']},"
" code: ${res.data['code']}",
};
} }
} }
// 发送私信 // 发送私信
static Future sendMsg({ static Future sendMsg({
int? senderUid, required int senderUid,
int? receiverId, required int receiverId,
int? receiverType, int? receiverType,
int? msgType, int? msgType,
dynamic content, dynamic content,
}) async { }) async {
String csrf = await Request.getCsrf(); String csrf = await Request.getCsrf();
Map<String, dynamic> params = await WbiSign().makSign({ var res = await Request().post(
'msg[sender_uid]': senderUid, Api.sendMsg,
'msg[receiver_id]': receiverId, data: {
'msg[receiver_type]': receiverType ?? 1, 'msg[sender_uid]': senderUid,
'msg[msg_type]': msgType ?? 1, 'msg[receiver_id]': receiverId,
'msg[msg_status]': 0, 'msg[receiver_type]': 1,
'msg[dev_id]': getDevId(), 'msg[msg_type]': 1,
'msg[timestamp]': DateTime.now().millisecondsSinceEpoch ~/ 1000, 'msg[msg_status]': 0,
'msg[new_face_version]': 0, 'msg[content]': jsonEncode(content),
'msg[content]': content, 'msg[timestamp]': DateTime.now().millisecondsSinceEpoch ~/ 1000,
'from_firework': 0, 'msg[new_face_version]': 0,
'build': 0, 'msg[dev_id]': getDevId(),
'mobi_app': 'web', 'from_firework': 0,
'csrf_token': csrf, 'build': 0,
'csrf': csrf, 'mobi_app': 'web',
}); 'csrf_token': csrf,
var res = 'csrf': csrf,
await Request().post(Api.sendMsg, queryParameters: <String, dynamic>{ },
...params, options: Options(
'csrf_token': csrf, contentType: Headers.formUrlEncodedContentType,
'csrf': csrf, ),
}, data: { );
'w_sender_uid': params['msg[sender_uid]'],
'w_receiver_id': params['msg[receiver_id]'],
'w_dev_id': params['msg[dev_id]'],
'w_rid': params['w_rid'],
'wts': params['wts'],
'csrf_token': csrf,
'csrf': csrf,
});
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { return {
'status': true, 'status': true,
'data': res.data['data'], 'data': res.data['data'],
}; };
} else { } else {
return { return {'status': false, 'date': [], 'msg': res.data['message']};
'status': false,
'date': [],
'msg': "message: ${res.data['message']},"
" msg: ${res.data['msg']},"
" code: ${res.data['code']}",
};
} }
} }
@ -220,4 +204,87 @@ class MsgHttp {
} }
return s.join(); return s.join();
} }
static Future removeSession({
int? talkerId,
}) async {
String csrf = await Request.getCsrf();
Map params = await WbiSign().makSign({
'talker_id': talkerId,
'session_type': 1,
'build': 0,
'mobi_app': 'web',
'csrf_token': csrf,
'csrf': csrf
});
var res = await Request().get(Api.removeSession, data: params);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {'status': false, 'date': [], 'msg': res.data['message']};
}
}
static Future unread() async {
var res = await Request().get(Api.unread);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {'status': false, 'date': [], 'msg': res.data['message']};
}
}
// 回复我的
static Future messageReply({
int? id,
int? replyTime,
}) async {
var params = {
if (id != null) 'id': id,
if (replyTime != null) 'reply_time': replyTime,
};
var res = await Request().get(Api.messageReplyAPi, data: params);
if (res.data['code'] == 0) {
try {
return {
'status': true,
'data': MessageReplyModel.fromJson(res.data['data']),
};
} catch (err) {
return {'status': false, 'date': [], 'msg': err.toString()};
}
} else {
return {'status': false, 'date': [], 'msg': res.data['message']};
}
}
// 收到的赞
static Future messageLike({
int? id,
int? likeTime,
}) async {
var params = {
if (id != null) 'id': id,
if (likeTime != null) 'like_time': likeTime,
};
var res = await Request().get(Api.messageLikeAPi, data: params);
if (res.data['code'] == 0) {
try {
return {
'status': true,
'data': MessageLikeModel.fromJson(res.data['data']),
};
} catch (err) {
return {'status': false, 'date': [], 'msg': err.toString()};
}
} else {
return {'status': false, 'date': [], 'msg': res.data['message']};
}
}
} }

View File

@ -75,6 +75,7 @@ class SearchHttp {
required page, required page,
String? order, String? order,
int? duration, int? duration,
int? tids,
}) async { }) async {
var reqData = { var reqData = {
'search_type': searchType.type, 'search_type': searchType.type,
@ -84,6 +85,7 @@ class SearchHttp {
'page': page, 'page': page,
if (order != null) 'order': order, if (order != null) 'order': order,
if (duration != null) 'duration': duration, if (duration != null) 'duration': duration,
if (tids != null && tids != -1) 'tids': tids,
}; };
var res = await Request().get(Api.searchByType, data: reqData); var res = await Request().get(Api.searchByType, data: reqData);
if (res.data['code'] == 0 && res.data['data']['numPages'] > 0) { if (res.data['code'] == 0 && res.data['data']['numPages'] > 0) {

View File

@ -17,7 +17,6 @@ import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/router/app_pages.dart'; import 'package:pilipala/router/app_pages.dart';
import 'package:pilipala/pages/main/view.dart'; import 'package:pilipala/pages/main/view.dart';
import 'package:pilipala/services/disable_battery_opt.dart';
import 'package:pilipala/services/service_locator.dart'; import 'package:pilipala/services/service_locator.dart';
import 'package:pilipala/utils/app_scheme.dart'; import 'package:pilipala/utils/app_scheme.dart';
import 'package:pilipala/utils/data.dart'; import 'package:pilipala/utils/data.dart';
@ -66,7 +65,6 @@ void main() async {
} }
PiliSchame.init(); PiliSchame.init();
DisableBatteryOpt();
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {

View File

@ -0,0 +1,93 @@
// 操作类型的枚举值:点赞 不喜欢 收藏 投币 稍后再看 下载封面 后台播放 听视频 分享 下载视频
import 'package:flutter/material.dart';
import 'package:get/get.dart';
enum ActionType {
like,
coin,
collect,
watchLater,
share,
dislike,
downloadCover,
copyLink,
// backgroundPlay,
// listenVideo,
// downloadVideo,
}
extension ActionTypeExtension on ActionType {
String get value => [
'like',
'coin',
'collect',
'watchLater',
'share',
'dislike',
'downloadCover',
'copyLink',
// 'backgroundPlay',
// 'listenVideo',
// 'downloadVideo',
][index];
String get label => [
'点赞视频',
'投币',
'收藏视频',
'稍后再看',
'视频分享',
'不喜欢',
'下载封面',
'复制链接',
// '后台播放',
// '听视频',
// '下载视频',
][index];
}
List<Map> actionMenuConfig = [
{
'icon': const Icon(Icons.thumb_up_alt_outlined),
'label': '点赞视频',
'value': ActionType.like,
},
{
'icon': Image.asset(
'assets/images/coin.png',
width: 26,
color: IconTheme.of(Get.context!).color!.withOpacity(0.65),
),
'label': '投币',
'value': ActionType.coin,
},
{
'icon': const Icon(Icons.star_border),
'label': '收藏视频',
'value': ActionType.collect,
},
{
'icon': const Icon(Icons.watch_later_outlined),
'label': '稍后再看',
'value': ActionType.watchLater,
},
{
'icon': const Icon(Icons.share),
'label': '视频分享',
'value': ActionType.share,
},
{
'icon': const Icon(Icons.thumb_down_alt_outlined),
'label': '不喜欢',
'value': ActionType.dislike,
},
{
'icon': const Icon(Icons.image_outlined),
'label': '下载封面',
'value': ActionType.downloadCover,
},
{
'icon': const Icon(Icons.link_outlined),
'label': '复制链接',
'value': ActionType.copyLink,
},
];

View File

@ -69,9 +69,10 @@ class RecVideoItemAppModel {
: null; : null;
// 由于app端api并不会直接返回与owner的关注状态 // 由于app端api并不会直接返回与owner的关注状态
// 所以借用推荐原因是否为“已关注”、“新关注”等判别关注状态从而与web端接口等效 // 所以借用推荐原因是否为“已关注”、“新关注”等判别关注状态从而与web端接口等效
RegExp regex = RegExp(r'已关注|新关注');
isFollowed = rcmdReason != null && isFollowed = rcmdReason != null &&
rcmdReason!.content != null && rcmdReason!.content != null &&
rcmdReason!.content!.contains('关注') regex.hasMatch(rcmdReason!.content!)
? 1 ? 1
: 0; : 0;
// 如果是就无需再显示推荐原因交由view统一处理即可 // 如果是就无需再显示推荐原因交由view统一处理即可

183
lib/models/msg/like.dart Normal file
View File

@ -0,0 +1,183 @@
class MessageLikeModel {
MessageLikeModel({
this.latest,
this.total,
});
Latest? latest;
Total? total;
factory MessageLikeModel.fromJson(Map<String, dynamic> json) =>
MessageLikeModel(
latest: json["latest"] == null ? null : Latest.fromJson(json["latest"]),
total: json["total"] == null ? null : Total.fromJson(json["total"]),
);
}
class Latest {
Latest({
this.items,
this.lastViewAt,
});
List? items;
int? lastViewAt;
factory Latest.fromJson(Map<String, dynamic> json) => Latest(
items: json["items"],
lastViewAt: json["last_view_at"],
);
}
class Total {
Total({
this.cursor,
this.items,
});
Cursor? cursor;
List<MessageLikeItem>? items;
factory Total.fromJson(Map<String, dynamic> json) => Total(
cursor: Cursor.fromJson(json['cursor']),
items: json["items"] == null
? []
: json["items"].map<MessageLikeItem>((e) {
return MessageLikeItem.fromJson(e);
}).toList(),
);
}
class Cursor {
Cursor({
this.id,
this.isEnd,
this.time,
});
int? id;
bool? isEnd;
int? time;
factory Cursor.fromJson(Map<String, dynamic> json) => Cursor(
id: json['id'],
isEnd: json['is_end'],
time: json['time'],
);
}
class MessageLikeItem {
MessageLikeItem({
this.id,
this.users,
this.item,
this.counts,
this.likeTime,
this.noticeState,
this.isExpand = false,
});
int? id;
List<MessageLikeUser>? users;
MessageLikeItemItem? item;
int? counts;
int? likeTime;
int? noticeState;
bool isExpand;
factory MessageLikeItem.fromJson(Map<String, dynamic> json) =>
MessageLikeItem(
id: json["id"],
users: json["users"] == null
? []
: json["users"].map<MessageLikeUser>((e) {
return MessageLikeUser.fromJson(e);
}).toList(),
item: json["item"] == null
? null
: MessageLikeItemItem.fromJson(json["item"]),
counts: json["counts"],
likeTime: json["like_time"],
noticeState: json["notice_state"],
);
}
class MessageLikeUser {
MessageLikeUser({
this.mid,
this.fans,
this.nickname,
this.avatar,
this.midLink,
this.follow,
});
int? mid;
int? fans;
String? nickname;
String? avatar;
String? midLink;
bool? follow;
factory MessageLikeUser.fromJson(Map<String, dynamic> json) =>
MessageLikeUser(
mid: json["mid"],
fans: json["fans"],
nickname: json["nickname"],
avatar: json["avatar"],
midLink: json["mid_link"],
follow: json["follow"],
);
}
class MessageLikeItemItem {
MessageLikeItemItem({
this.itemId,
this.pid,
this.type,
this.business,
this.businessId,
this.replyBusinessId,
this.likeBusinessId,
this.title,
this.desc,
this.image,
this.uri,
this.detailName,
this.nativeUri,
this.ctime,
});
int? itemId;
int? pid;
String? type;
String? business;
int? businessId;
int? replyBusinessId;
int? likeBusinessId;
String? title;
String? desc;
String? image;
String? uri;
String? detailName;
String? nativeUri;
int? ctime;
factory MessageLikeItemItem.fromJson(Map<String, dynamic> json) =>
MessageLikeItemItem(
itemId: json["item_id"],
pid: json["pid"],
type: json["type"],
business: json["business"],
businessId: json["business_id"],
replyBusinessId: json["reply_business_id"],
likeBusinessId: json["like_business_id"],
title: json["title"],
desc: json["desc"],
image: json["image"],
uri: json["uri"],
detailName: json["detail_name"],
nativeUri: json["native_uri"],
ctime: json["ctime"],
);
}

168
lib/models/msg/reply.dart Normal file
View File

@ -0,0 +1,168 @@
class MessageReplyModel {
MessageReplyModel({
this.cursor,
this.items,
});
Cursor? cursor;
List<MessageReplyItem>? items;
MessageReplyModel.fromJson(Map<String, dynamic> json) {
cursor = Cursor.fromJson(json['cursor']);
items = json["items"] != null
? json["items"].map<MessageReplyItem>((e) {
return MessageReplyItem.fromJson(e);
}).toList()
: [];
}
}
class Cursor {
Cursor({
this.id,
this.isEnd,
this.time,
});
int? id;
bool? isEnd;
int? time;
Cursor.fromJson(Map<String, dynamic> json) {
id = json['id'];
isEnd = json['is_end'];
time = json['time'];
}
}
class MessageReplyItem {
MessageReplyItem({
this.count,
this.id,
this.isMulti,
this.item,
this.replyTime,
this.user,
});
int? count;
int? id;
int? isMulti;
ReplyContentItem? item;
int? replyTime;
ReplyUser? user;
MessageReplyItem.fromJson(Map<String, dynamic> json) {
count = json['count'];
id = json['id'];
isMulti = json['is_multi'];
item = ReplyContentItem.fromJson(json["item"]);
replyTime = json['reply_time'];
user = ReplyUser.fromJson(json['user']);
}
}
class ReplyContentItem {
ReplyContentItem({
this.subjectId,
this.rootId,
this.sourceId,
this.targetId,
this.type,
this.businessId,
this.business,
this.title,
this.desc,
this.image,
this.uri,
this.nativeUri,
this.detailTitle,
this.rootReplyContent,
this.sourceContent,
this.targetReplyContent,
this.atDetails,
this.topicDetails,
this.hideReplyButton,
this.hideLikeButton,
this.likeState,
this.danmu,
this.message,
});
int? subjectId;
int? rootId;
int? sourceId;
int? targetId;
String? type;
int? businessId;
String? business;
String? title;
String? desc;
String? image;
String? uri;
String? nativeUri;
String? detailTitle;
String? rootReplyContent;
String? sourceContent;
String? targetReplyContent;
List? atDetails;
List? topicDetails;
bool? hideReplyButton;
bool? hideLikeButton;
int? likeState;
String? danmu;
String? message;
ReplyContentItem.fromJson(Map<String, dynamic> json) {
subjectId = json['subject_id'];
rootId = json['root_id'];
sourceId = json['source_id'];
targetId = json['target_id'];
type = json['type'];
businessId = json['business_id'];
business = json['business'];
title = json['title'];
desc = json['desc'];
image = json['image'];
uri = json['uri'];
nativeUri = json['native_uri'];
detailTitle = json['detail_title'];
rootReplyContent = json['root_reply_content'];
sourceContent = json['source_content'];
targetReplyContent = json['target_reply_content'];
atDetails = json['at_details'];
topicDetails = json['topic_details'];
hideReplyButton = json['hide_reply_button'];
hideLikeButton = json['hide_like_button'];
likeState = json['like_state'];
danmu = json['danmu'];
message = json['message'];
}
}
class ReplyUser {
ReplyUser({
this.mid,
this.fans,
this.nickname,
this.avatar,
this.midLink,
this.follow,
});
int? mid;
int? fans;
String? nickname;
String? avatar;
String? midLink;
bool? follow;
ReplyUser.fromJson(Map<String, dynamic> json) {
mid = json['mid'];
fans = json['fans'];
nickname = json['nickname'];
avatar = json['avatar'];
midLink = json['mid_link'];
follow = json['follow'];
}
}

View File

@ -20,10 +20,10 @@ class IntroDetail extends StatelessWidget {
sheetHeight = localCache.get('sheetHeight'); sheetHeight = localCache.get('sheetHeight');
TextStyle smallTitle = TextStyle( TextStyle smallTitle = TextStyle(
fontSize: 12, fontSize: 12,
color: Theme.of(context).colorScheme.onBackground, color: Theme.of(context).colorScheme.onSurface,
); );
return Container( return Container(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.surface,
padding: const EdgeInsets.only(left: 14, right: 14), padding: const EdgeInsets.only(left: 14, right: 14),
height: sheetHeight, height: sheetHeight,
child: Column( child: Column(

View File

@ -167,8 +167,7 @@ class _DynamicsPageState extends State<DynamicsPage>
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
thumbDecoration: BoxDecoration( thumbDecoration: BoxDecoration(
color: color: Theme.of(context).colorScheme.surface,
Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),

View File

@ -19,7 +19,7 @@ Widget addWidget(item, context, type, {floor = 1}) {
}; };
Color bgColor = floor == 1 Color bgColor = floor == 1
? Theme.of(context).dividerColor.withOpacity(0.08) ? Theme.of(context).dividerColor.withOpacity(0.08)
: Theme.of(context).colorScheme.background; : Theme.of(context).colorScheme.surface;
switch (type) { switch (type) {
case 'ADDITIONAL_TYPE_UGC': case 'ADDITIONAL_TYPE_UGC':
// 转发的投稿 // 转发的投稿

View File

@ -52,7 +52,7 @@ class AuthorPanel extends StatelessWidget {
color: item.modules.moduleAuthor!.vip != null && color: item.modules.moduleAuthor!.vip != null &&
item.modules.moduleAuthor!.vip['status'] > 0 item.modules.moduleAuthor!.vip['status'] > 0
? const Color.fromARGB(255, 251, 100, 163) ? const Color.fromARGB(255, 251, 100, 163)
: Theme.of(context).colorScheme.onBackground, : Theme.of(context).colorScheme.onSurface,
fontSize: Theme.of(context).textTheme.titleSmall!.fontSize, fontSize: Theme.of(context).textTheme.titleSmall!.fontSize,
), ),
), ),

View File

@ -69,7 +69,7 @@ class _UpPanelState extends State<UpPanel> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.surface,
padding: const EdgeInsets.only(left: 16, right: 16), padding: const EdgeInsets.only(left: 16, right: 16),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -95,7 +95,7 @@ class _UpPanelState extends State<UpPanel> {
), ),
Container( Container(
height: 90, height: 90,
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.surface,
child: Row( child: Row(
children: [ children: [
Flexible( Flexible(

View File

@ -1,11 +1,14 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:encrypt/encrypt.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/http/login.dart'; import 'package:pilipala/http/login.dart';
import 'package:gt3_flutter_plugin/gt3_flutter_plugin.dart'; import 'package:gt3_flutter_plugin/gt3_flutter_plugin.dart';
import 'package:pilipala/models/login/index.dart'; import 'package:pilipala/models/login/index.dart';
import 'package:pilipala/utils/login.dart';
class LoginPageController extends GetxController { class LoginPageController extends GetxController {
final GlobalKey mobFormKey = GlobalKey<FormState>(); final GlobalKey mobFormKey = GlobalKey<FormState>();
@ -26,9 +29,23 @@ class LoginPageController extends GetxController {
final Gt3FlutterPlugin captcha = Gt3FlutterPlugin(); final Gt3FlutterPlugin captcha = Gt3FlutterPlugin();
// 倒计时60s
RxInt seconds = 60.obs;
Timer? timer;
RxBool smsCodeSendStatus = false.obs;
// 默认密码登录 // 默认密码登录
RxInt loginType = 0.obs; RxInt loginType = 0.obs;
late String captchaKey;
late int tel;
late int webSmsCode;
RxInt validSeconds = 180.obs;
Timer? validTimer;
late String qrcodeKey;
// 监听pageView切换 // 监听pageView切换
void onPageChange(int index) { void onPageChange(int index) {
currentIndex.value = index; currentIndex.value = index;
@ -43,6 +60,7 @@ class LoginPageController extends GetxController {
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );
passwordTextFieldNode.requestFocus(); passwordTextFieldNode.requestFocus();
(mobFormKey.currentState as FormState).save();
} }
} }
@ -86,18 +104,64 @@ class LoginPageController extends GetxController {
} }
} }
// 验证码登录 // web端密码登录
void loginInByCode() { void loginInByWebPassword() async {
if ((msgCodeFormKey.currentState as FormState).validate()) {} if ((passwordFormKey.currentState as FormState).validate()) {
getCaptcha((data) async {
CaptchaDataModel captchaData = data;
var webKeyRes = await LoginHttp.getWebKey();
if (webKeyRes['status']) {
String rhash = webKeyRes['data']['hash'];
String key = webKeyRes['data']['key'];
dynamic publicKey = RSAKeyParser().parse(key);
String passwordEncryptyed = Encrypter(RSA(publicKey: publicKey))
.encrypt(rhash + passwordTextController.text)
.base64;
var res = await LoginHttp.loginInByWebPwd(
username: tel,
password: passwordEncryptyed,
token: captchaData.token!,
challenge: captchaData.geetest!.challenge!,
validate: captchaData.validate!,
seccode: captchaData.seccode!,
);
if (res['status']) {
await LoginUtils.confirmLogin('', null);
} else {
SmartDialog.showToast(res['msg']);
}
} else {
SmartDialog.showToast(webKeyRes['msg']);
}
});
}
} }
// app端验证码 // web端验证码登录
void getMsgCode() async { void loginInByCode() async {
if ((msgCodeFormKey.currentState as FormState).validate()) {
(msgCodeFormKey.currentState as FormState).save();
var res = await LoginHttp.loginInByWebSmsCode(
cid: 86,
tel: tel,
code: webSmsCode,
captchaKey: captchaKey,
);
if (res['status']) {
await LoginUtils.confirmLogin('', null);
} else {
SmartDialog.showToast(res['msg']);
}
}
}
// 获取app端验证码
void getAppMsgCode() async {
getCaptcha((data) async { getCaptcha((data) async {
CaptchaDataModel captchaData = data; CaptchaDataModel captchaData = data;
var res = await LoginHttp.sendAppSmsCode( var res = await LoginHttp.sendAppSmsCode(
cid: 86, cid: 86,
tel: 13734077064, tel: tel,
token: captchaData.token!, token: captchaData.token!,
challenge: captchaData.geetest!.challenge!, challenge: captchaData.geetest!.challenge!,
validate: captchaData.validate!, validate: captchaData.validate!,
@ -121,7 +185,7 @@ class LoginPageController extends GetxController {
captcha.addEventHandler(onShow: (Map<String, dynamic> message) async { captcha.addEventHandler(onShow: (Map<String, dynamic> message) async {
SmartDialog.dismiss(); SmartDialog.dismiss();
}, onClose: (Map<String, dynamic> message) async { }, onClose: (Map<String, dynamic> message) async {
SmartDialog.showToast('关闭验证'); SmartDialog.showToast('取消验证');
}, onResult: (Map<String, dynamic> message) async { }, onResult: (Map<String, dynamic> message) async {
debugPrint("Captcha result: $message"); debugPrint("Captcha result: $message");
String code = message["code"]; String code = message["code"];
@ -201,4 +265,72 @@ class LoginPageController extends GetxController {
captcha.startCaptcha(registerData); captcha.startCaptcha(registerData);
} else {} } else {}
} }
// 获取web端验证码
void getWebMsgCode() async {
getCaptcha((data) async {
CaptchaDataModel captchaData = data;
var res = await LoginHttp.sendWebSmsCode(
cid: 86,
tel: tel,
token: captchaData.token!,
challenge: captchaData.geetest!.challenge!,
validate: captchaData.validate!,
seccode: captchaData.seccode!,
);
if (res['status']) {
captchaKey = res['data']['captcha_key'];
SmartDialog.showToast('验证码已发送');
// 倒计时60s
smsCodeSendStatus.value = true;
startTimer();
} else {
SmartDialog.showToast(res['msg']);
}
});
}
// 验证码倒计时
void startTimer() {
timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (seconds.value > 0) {
seconds.value--;
} else {
seconds.value = 60;
smsCodeSendStatus.value = false;
timer.cancel();
}
});
}
// 获取登录二维码
Future getWebQrcode() async {
var res = await LoginHttp.getWebQrcode();
validSeconds.value = 180;
if (res['status']) {
qrcodeKey = res['data']['qrcode_key'];
validTimer = Timer.periodic(const Duration(seconds: 1), (validTimer) {
if (validSeconds.value > 0) {
validSeconds.value--;
queryWebQrcodeStatus();
} else {
getWebQrcode();
validTimer.cancel();
}
});
return res;
} else {
SmartDialog.showToast(res['msg']);
}
}
// 轮询二维码登录状态
Future queryWebQrcodeStatus() async {
var res = await LoginHttp.queryWebQrcodeStatus(qrcodeKey);
if (res['status']) {
await LoginUtils.confirmLogin('', null);
validTimer?.cancel();
Get.back();
}
}
} }

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'controller.dart'; import 'controller.dart';
@ -14,8 +15,10 @@ class _LoginPageState extends State<LoginPage> {
final LoginPageController _loginPageCtr = Get.put(LoginPageController()); final LoginPageController _loginPageCtr = Get.put(LoginPageController());
@override @override
void initState() { void dispose() {
super.initState(); _loginPageCtr.validTimer?.cancel();
_loginPageCtr.timer?.cancel();
super.dispose();
} }
@override @override
@ -37,6 +40,107 @@ class _LoginPageState extends State<LoginPage> {
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
), ),
), ),
actions: [
IconButton(
tooltip: '浏览器打开',
onPressed: () {
Get.offNamed(
'/webview',
parameters: {
'url': 'https://passport.bilibili.com/h5-app/passport/login',
'type': 'login',
'pageTitle': '登录bilibili',
},
);
},
icon: const Icon(Icons.language, size: 20),
),
IconButton(
tooltip: '二维码登录',
onPressed: () {
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, StateSetter setState) {
return AlertDialog(
title: Row(
children: [
const Text('扫码登录'),
IconButton(
onPressed: () {
setState(() {});
},
icon: const Icon(Icons.refresh),
),
],
),
contentPadding: const EdgeInsets.fromLTRB(0, 0, 0, 4),
content: AspectRatio(
aspectRatio: 1,
child: Container(
width: 200,
padding: const EdgeInsets.all(12),
child: FutureBuilder(
future: _loginPageCtr.getWebQrcode(),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
Map data = snapshot.data as Map;
return QrImageView(
data: data['data']['url'],
backgroundColor: Colors.white,
);
} else {
return const Center(
child: SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(),
),
);
}
},
),
),
),
actions: [
TextButton(
onPressed: () {},
child: Obx(() {
return Text(
'有效期: ${_loginPageCtr.validSeconds.value}s',
style: Theme.of(context).textTheme.titleMedium,
);
}),
),
TextButton(
onPressed: () {},
child: Text(
'检查登录状态',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.titleMedium!
.fontSize,
),
),
)
],
);
});
},
).then((value) {
_loginPageCtr.validTimer!.cancel();
});
},
icon: const Icon(Icons.qr_code, size: 20),
),
const SizedBox(width: 22),
],
), ),
body: PageView( body: PageView(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
@ -64,17 +168,9 @@ class _LoginPageState extends State<LoginPage> {
fontSize: 34, fontSize: 34,
fontWeight: FontWeight.w500), fontWeight: FontWeight.w500),
), ),
Row( Text(
children: [ '请使用您的 BiliBili 账号登录。',
Text( style: Theme.of(context).textTheme.titleSmall!,
'请使用您的 BiliBili 账号登录。',
style: Theme.of(context).textTheme.titleSmall!,
),
GestureDetector(
onTap: () {},
child: const Icon(Icons.info_outline, size: 16),
)
],
), ),
Container( Container(
margin: const EdgeInsets.only(top: 38, bottom: 15), margin: const EdgeInsets.only(top: 38, bottom: 15),
@ -93,35 +189,12 @@ class _LoginPageState extends State<LoginPage> {
validator: (v) { validator: (v) {
return v!.trim().isNotEmpty ? null : "手机号码不能为空"; return v!.trim().isNotEmpty ? null : "手机号码不能为空";
}, },
onSaved: (val) { onSaved: (val) => _loginPageCtr.tel = int.parse(val!),
print(val);
},
onEditingComplete: () { onEditingComplete: () {
_loginPageCtr.nextStep(); _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(), const Spacer(),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -236,7 +309,7 @@ class _LoginPageState extends State<LoginPage> {
.primary, // 设置按钮背景色 .primary, // 设置按钮背景色
), ),
onPressed: () => onPressed: () =>
_loginPageCtr.loginInByAppPassword(), _loginPageCtr.loginInByWebPassword(),
child: const Text('确认登录'), child: const Text('确认登录'),
) )
], ],
@ -308,21 +381,28 @@ class _LoginPageState extends State<LoginPage> {
? null ? null
: "验证码不能为空"; : "验证码不能为空";
}, },
onSaved: (val) { onSaved: (val) => _loginPageCtr.webSmsCode =
print(val); int.parse(val!),
},
), ),
Positioned( Obx(() {
right: 8, return Positioned(
top: 4, right: 8,
child: Center( top: 0,
child: TextButton( child: Center(
onPressed: () => child: TextButton(
_loginPageCtr.getMsgCode(), onPressed: _loginPageCtr
child: const Text('获取验证码'), .smsCodeSendStatus.value
? null
: () =>
_loginPageCtr.getWebMsgCode(),
child: _loginPageCtr
.smsCodeSendStatus.value
? Text(
'重新获取(${_loginPageCtr.seconds.value}s)')
: const Text('获取验证码')),
), ),
), );
), })
], ],
), ),
), ),

View File

@ -90,9 +90,8 @@ class _MemberPageState extends State<MemberPage>
() => Text( () => Text(
_memberController.memberInfo.value.name ?? '', _memberController.memberInfo.value.name ?? '',
style: TextStyle( style: TextStyle(
color: Theme.of(context) color:
.colorScheme Theme.of(context).colorScheme.onSurface,
.onBackground,
fontSize: 14), fontSize: 14),
), ),
), ),

View File

@ -208,7 +208,17 @@ class ProfilePanel extends StatelessWidget {
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: TextButton( child: TextButton(
onPressed: () {}, onPressed: () {
Get.toNamed(
'/whisperDetail',
parameters: {
'name': memberInfo.name!,
'face': memberInfo.face!,
'mid': memberInfo.mid.toString(),
'heroTag': ctr.heroTag!,
},
);
},
style: TextButton.styleFrom( style: TextButton.styleFrom(
backgroundColor: Theme.of(context) backgroundColor: Theme.of(context)
.colorScheme .colorScheme

View File

@ -0,0 +1,3 @@
import 'package:get/get.dart';
class MessageAtController extends GetxController {}

View File

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

View File

@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
class MessageAtPage extends StatefulWidget {
const MessageAtPage({super.key});
@override
State<MessageAtPage> createState() => _MessageAtPageState();
}
class _MessageAtPageState extends State<MessageAtPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('@我的'),
),
);
}
}

View File

@ -0,0 +1,30 @@
import 'package:get/get.dart';
import 'package:pilipala/http/msg.dart';
import 'package:pilipala/models/msg/like.dart';
class MessageLikeController extends GetxController {
Cursor? cursor;
RxList<MessageLikeItem> likeItems = <MessageLikeItem>[].obs;
Future queryMessageLike({String type = 'init'}) async {
if (cursor != null && cursor!.isEnd == true) {
return {};
}
var params = {
if (type == 'onLoad') 'id': cursor!.id,
if (type == 'onLoad') 'likeTime': cursor!.time,
};
var res = await MsgHttp.messageLike(
id: params['id'], likeTime: params['likeTime']);
if (res['status']) {
cursor = res['data'].total.cursor;
likeItems.addAll(res['data'].total.items);
}
return res;
}
Future expandedUsersAvatar(i) async {
likeItems[i].isExpand = !likeItems[i].isExpand;
likeItems.refresh();
}
}

View File

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

View File

@ -0,0 +1,319 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/msg/like.dart';
import 'package:pilipala/utils/utils.dart';
import 'controller.dart';
class MessageLikePage extends StatefulWidget {
const MessageLikePage({super.key});
@override
State<MessageLikePage> createState() => _MessageLikePageState();
}
class _MessageLikePageState extends State<MessageLikePage> {
final MessageLikeController _messageLikeCtr =
Get.put(MessageLikeController());
late Future _futureBuilderFuture;
final ScrollController scrollController = ScrollController();
@override
void initState() {
super.initState();
_futureBuilderFuture = _messageLikeCtr.queryMessageLike();
scrollController.addListener(
() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
_messageLikeCtr.queryMessageLike(type: 'onLoad');
});
}
},
);
}
@override
void dispose() {
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('收到的赞'),
),
body: RefreshIndicator(
onRefresh: () async {
await _messageLikeCtr.queryMessageLike(type: 'init');
},
child: FutureBuilder(
future: _futureBuilderFuture,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
if (snapshot.data['status']) {
final likeItems = _messageLikeCtr.likeItems;
return Obx(
() => ListView.separated(
controller: scrollController,
itemBuilder: (context, index) => LikeItem(
item: likeItems[index],
index: index,
messageLikeCtr: _messageLikeCtr,
),
itemCount: likeItems.length,
separatorBuilder: (BuildContext context, int index) {
return Divider(
indent: 66,
endIndent: 14,
height: 1,
color: Colors.grey.withOpacity(0.1),
);
},
),
);
} else {
// 请求错误
return CustomScrollView(
slivers: [
HttpError(
errMsg: snapshot.data['msg'],
fn: () {
setState(() {
_futureBuilderFuture =
_messageLikeCtr.queryMessageLike();
});
},
)
],
);
}
} else {
return const SizedBox();
}
},
),
),
);
}
}
class LikeItem extends StatelessWidget {
final MessageLikeItem item;
final int index;
final MessageLikeController messageLikeCtr;
const LikeItem(
{super.key,
required this.item,
required this.index,
required this.messageLikeCtr});
@override
Widget build(BuildContext context) {
Color outline = Theme.of(context).colorScheme.outline;
final nickNameList = item.users!.map((e) => e.nickname).take(2).toList();
int usersLen = item.users!.length > 3 ? 3 : item.users!.length;
final String bvid = item.item!.uri!.split('/').last;
// 页码
final String page =
item.item!.nativeUri!.split('page=').last.split('&').first;
// 根评论id
final String commentRootId =
item.item!.nativeUri!.split('comment_root_id=').last.split('&').first;
// 二级评论id
final String commentSecondaryId =
item.item!.nativeUri!.split('comment_secondary_id=').last;
return InkWell(
onTap: () async {
try {
final int cid = await SearchHttp.ab2c(bvid: bvid);
final String heroTag = Utils.makeHeroTag(bvid);
Get.toNamed<dynamic>(
'/video?bvid=$bvid&cid=$cid',
arguments: <String, String?>{
'pic': '',
'heroTag': heroTag,
},
);
} catch (_) {
SmartDialog.showToast('视频可能失效了');
}
},
child: Stack(
children: [
Padding(
padding: const EdgeInsets.all(14),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
if (usersLen == 1) {
final String heroTag =
Utils.makeHeroTag(item.users!.first.mid);
Get.toNamed('/member?mid=${item.users!.first.mid}',
arguments: {
'face': item.users!.first.avatar,
'heroTag': heroTag
});
} else {
messageLikeCtr.expandedUsersAvatar(index);
}
},
// 多个头像层叠
child: SizedBox(
width: 50,
height: 50,
child: Stack(
children: [
for (var i = 0; i < usersLen; i++)
Positioned(
top: i % 2 * (50 / (usersLen >= 2 ? 2 : 1)),
left: i / 2 * (50 / (usersLen >= 2 ? 2 : 1)),
child: NetworkImgLayer(
width: 50 / (usersLen >= 2 ? 2 : 1),
height: 50 / (usersLen >= 2 ? 2 : 1),
type: 'avatar',
src: item.users![i].avatar,
),
),
],
),
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text.rich(TextSpan(children: [
TextSpan(text: nickNameList.join('')),
const TextSpan(text: ' '),
if (item.users!.length > 1)
TextSpan(
text: '等总计${item.users!.length}',
style: TextStyle(color: outline),
),
TextSpan(
text: '赞了我的评论',
style: TextStyle(color: outline),
),
])),
const SizedBox(height: 4),
Text(
Utils.dateFormat(item.likeTime!, formatType: 'detail'),
style: TextStyle(color: outline),
),
],
),
),
const SizedBox(width: 25),
if (item.item!.type! == 'reply')
Container(
width: 60,
height: 60,
padding: const EdgeInsets.all(4),
child: Text(
item.item!.title!,
maxLines: 4,
style: const TextStyle(fontSize: 12, letterSpacing: 0.3),
overflow: TextOverflow.ellipsis,
),
),
if (item.item!.type! == 'video')
NetworkImgLayer(
width: 60,
height: 60,
src: item.item!.image,
),
],
),
),
Positioned(
top: 0,
right: 0,
bottom: 0,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: item.isExpand ? Get.size.width - 74 : 0,
color: Theme.of(context).colorScheme.secondaryContainer,
child: ListView.builder(
itemCount: item.users!.length,
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext context, int i) {
return Padding(
padding: EdgeInsets.fromLTRB(i == 0 ? 12 : 4, 8, 4, 0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onTap: () {
final String heroTag =
Utils.makeHeroTag(item.users![i].mid);
Get.toNamed(
'/member?mid=${item.users![i].mid}',
arguments: {
'face': item.users![i].avatar,
'heroTag': heroTag
},
);
},
child: NetworkImgLayer(
width: 42,
height: 42,
type: 'avatar',
src: item.users![i].avatar,
),
),
const SizedBox(height: 6),
SizedBox(
width: 68,
child: Text(
textAlign: TextAlign.center,
item.users![i].nickname!,
maxLines: 1,
overflow: TextOverflow.clip,
style: TextStyle(color: outline),
),
),
],
),
);
},
),
),
),
Positioned(
top: 0,
left: 0,
bottom: 0,
child: GestureDetector(
onTap: () {
messageLikeCtr.expandedUsersAvatar(index);
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: item.isExpand ? 74 : 0,
color: Colors.black.withOpacity(0.3),
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,25 @@
import 'package:get/get.dart';
import 'package:pilipala/http/msg.dart';
import 'package:pilipala/models/msg/reply.dart';
class MessageReplyController extends GetxController {
Cursor? cursor;
RxList<MessageReplyItem> replyItems = <MessageReplyItem>[].obs;
Future queryMessageReply({String type = 'init'}) async {
if (cursor != null && cursor!.isEnd == true) {
return {};
}
var params = {
if (type == 'onLoad') 'id': cursor!.id,
if (type == 'onLoad') 'replyTime': cursor!.time,
};
var res = await MsgHttp.messageReply(
id: params['id'], replyTime: params['replyTime']);
if (res['status']) {
cursor = res['data'].cursor;
replyItems.addAll(res['data'].items);
}
return res;
}
}

View File

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

View File

@ -0,0 +1,272 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/msg/reply.dart';
import 'package:pilipala/utils/utils.dart';
import 'controller.dart';
class MessageReplyPage extends StatefulWidget {
const MessageReplyPage({super.key});
@override
State<MessageReplyPage> createState() => _MessageReplyPageState();
}
class _MessageReplyPageState extends State<MessageReplyPage> {
final MessageReplyController _messageReplyCtr =
Get.put(MessageReplyController());
late Future _futureBuilderFuture;
final ScrollController scrollController = ScrollController();
@override
void initState() {
super.initState();
_futureBuilderFuture = _messageReplyCtr.queryMessageReply();
scrollController.addListener(
() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
_messageReplyCtr.queryMessageReply(type: 'onLoad');
});
}
},
);
}
@override
void dispose() {
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('回复我的'),
),
body: RefreshIndicator(
onRefresh: () async {
await _messageReplyCtr.queryMessageReply(type: 'init');
},
child: FutureBuilder(
future: _futureBuilderFuture,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
if (snapshot.data['status']) {
final replyItems = _messageReplyCtr.replyItems;
return Obx(
() => ListView.separated(
controller: scrollController,
itemBuilder: (context, index) =>
ReplyItem(item: replyItems[index]),
itemCount: replyItems.length,
separatorBuilder: (BuildContext context, int index) {
return Divider(
indent: 66,
endIndent: 14,
height: 1,
color: Colors.grey.withOpacity(0.1),
);
},
),
);
} else {
// 请求错误
return CustomScrollView(
slivers: [
HttpError(
errMsg: snapshot.data['msg'],
fn: () {
setState(() {
_futureBuilderFuture =
_messageReplyCtr.queryMessageReply();
});
},
)
],
);
}
} else {
return const SizedBox();
}
},
),
),
);
}
}
class ReplyItem extends StatelessWidget {
final MessageReplyItem item;
const ReplyItem({super.key, required this.item});
@override
Widget build(BuildContext context) {
Color outline = Theme.of(context).colorScheme.outline;
final String heroTag = Utils.makeHeroTag(item.user!.mid);
final String bvid = item.item!.uri!.split('/').last;
// 页码
final String page =
item.item!.nativeUri!.split('page=').last.split('&').first;
// 根评论id
final String commentRootId =
item.item!.nativeUri!.split('comment_root_id=').last.split('&').first;
// 二级评论id
final String commentSecondaryId =
item.item!.nativeUri!.split('comment_secondary_id=').last;
return InkWell(
onTap: () async {
final int cid = await SearchHttp.ab2c(bvid: bvid);
final String heroTag = Utils.makeHeroTag(bvid);
Get.toNamed<dynamic>(
'/video?bvid=$bvid&cid=$cid',
arguments: <String, String?>{
'pic': '',
'heroTag': heroTag,
},
);
},
child: Padding(
padding: const EdgeInsets.all(14),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
Get.toNamed('/member?mid=${item.user!.mid}',
arguments: {'face': item.user!.avatar, 'heroTag': heroTag});
},
child: Hero(
tag: heroTag,
child: NetworkImgLayer(
width: 42,
height: 42,
type: 'avatar',
src: item.user!.avatar,
),
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text.rich(TextSpan(children: [
TextSpan(text: item.user!.nickname!),
const TextSpan(text: ' '),
if (item.item!.type! == 'video')
TextSpan(
text: '对我的视频发表了评论', style: TextStyle(color: outline)),
if (item.item!.type! == 'reply')
TextSpan(
text: '回复了我的评论',
style: TextStyle(color: outline),
),
])),
const SizedBox(height: 6),
Text.rich(
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(letterSpacing: 0.3),
buildContent(context, item.item)),
if (item.item!.targetReplyContent != '') ...[
const SizedBox(height: 2),
Text(
item.item!.targetReplyContent!,
style: TextStyle(color: outline),
),
],
const SizedBox(height: 4),
Row(
children: [
Text(
Utils.dateFormat(item.replyTime!, formatType: 'detail'),
style: TextStyle(color: outline),
),
const SizedBox(width: 16),
Text('回复', style: TextStyle(color: outline)),
],
)
],
),
),
const SizedBox(width: 25),
if (item.item!.type! == 'reply')
Container(
width: 60,
height: 80,
padding: const EdgeInsets.all(4),
child: Text(
item.item!.rootReplyContent!,
maxLines: 4,
style: const TextStyle(fontSize: 12, letterSpacing: 0.3),
overflow: TextOverflow.ellipsis,
),
),
if (item.item!.type! == 'video')
NetworkImgLayer(
width: 60,
height: 60,
src: item.item!.image,
),
],
),
),
);
}
}
InlineSpan buildContent(BuildContext context, item) {
List? atDetails = item!.atDetails;
final List<InlineSpan> spanChilds = <InlineSpan>[];
if (atDetails!.isNotEmpty) {
final String patternStr =
atDetails.map<String>((e) => '@${e['nickname']}').toList().join('|');
final RegExp regExp = RegExp(patternStr);
item.sourceContent!.splitMapJoin(
regExp,
onMatch: (Match match) {
spanChilds.add(
TextSpan(
text: match.group(0),
recognizer: TapGestureRecognizer()
..onTap = () {
var currentUser = atDetails
.where((e) => e['nickname'] == match.group(0)!.substring(1))
.first;
Get.toNamed('/member?mid=${currentUser['mid']}', arguments: {
'face': currentUser['avatar'],
});
},
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
);
return '';
},
onNonMatch: (String nonMatch) {
spanChilds.add(
TextSpan(text: nonMatch),
);
return '';
},
);
} else {
spanChilds.add(
TextSpan(text: item.sourceContent),
);
}
return TextSpan(children: spanChilds);
}

View File

@ -0,0 +1,3 @@
import 'package:get/get.dart';
class MessageSystemController extends GetxController {}

View File

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

View File

@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
class MessageSystemPage extends StatefulWidget {
const MessageSystemPage({super.key});
@override
State<MessageSystemPage> createState() => _MessageSystemPageState();
}
class _MessageSystemPageState extends State<MessageSystemPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('系统通知'),
),
);
}
}

View File

@ -6,7 +6,6 @@ import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/common/theme_type.dart'; import 'package:pilipala/models/common/theme_type.dart';
import 'package:pilipala/models/user/info.dart'; import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/models/user/stat.dart'; import 'package:pilipala/models/user/stat.dart';
import 'package:pilipala/utils/route_push.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
class MineController extends GetxController { class MineController extends GetxController {
@ -34,8 +33,7 @@ class MineController extends GetxController {
onLogin() async { onLogin() async {
if (!userLogin.value) { if (!userLogin.value) {
RoutePush.loginPush(); Get.toNamed('/loginPage', preventDuplicates: false);
// Get.toNamed('/loginPage');
} else { } else {
int mid = userInfo.value.mid!; int mid = userInfo.value.mid!;
String face = userInfo.value.face!; String face = userInfo.value.face!;
@ -59,8 +57,6 @@ class MineController extends GetxController {
} else { } else {
resetUserInfo(); resetUserInfo();
} }
} else {
resetUserInfo();
} }
await queryUserStatOwner(); await queryUserStatOwner();
return res; return res;

View File

@ -5,18 +5,22 @@ class SearchText extends StatelessWidget {
final Function? onSelect; final Function? onSelect;
final int? searchTextIdx; final int? searchTextIdx;
final Function? onLongSelect; final Function? onLongSelect;
final bool isSelect;
const SearchText({ const SearchText({
super.key, super.key,
this.searchText, this.searchText,
this.onSelect, this.onSelect,
this.searchTextIdx, this.searchTextIdx,
this.onLongSelect, this.onLongSelect,
this.isSelect = false,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return Material(
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5), color: isSelect
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
child: Padding( child: Padding(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
@ -34,7 +38,10 @@ class SearchText extends StatelessWidget {
child: Text( child: Text(
searchText!, searchText!,
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant), color: isSelect
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant,
),
), ),
), ),
), ),

View File

@ -16,14 +16,18 @@ class SearchPanelController extends GetxController {
RxString order = ''.obs; RxString order = ''.obs;
// 视频时长筛选 仅用于搜索视频 // 视频时长筛选 仅用于搜索视频
RxInt duration = 0.obs; RxInt duration = 0.obs;
// 视频分区筛选 仅用于搜索视频 -1时不传
RxInt tids = (-1).obs;
Future onSearch({type = 'init'}) async { Future onSearch({type = 'init'}) async {
var result = await SearchHttp.searchByType( var result = await SearchHttp.searchByType(
searchType: searchType!, searchType: searchType!,
keyword: keyword!, keyword: keyword!,
page: page.value, page: page.value,
order: searchType!.type != 'video' ? null : order.value, order: searchType!.type != 'video' ? null : order.value,
duration: searchType!.type != 'video' ? null : duration.value); duration: searchType!.type != 'video' ? null : duration.value,
tids: searchType!.type != 'video' ? null : tids.value,
);
if (result['status']) { if (result['status']) {
if (type == 'onRefresh') { if (type == 'onRefresh') {
resultList.value = result['data'].list; resultList.value = result['data'].list;

View File

@ -3,6 +3,7 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/video_card_h.dart'; import 'package:pilipala/common/widgets/video_card_h.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/pages/search/widgets/search_text.dart';
import 'package:pilipala/pages/search_panel/index.dart'; import 'package:pilipala/pages/search_panel/index.dart';
class SearchVideoPanel extends StatelessWidget { class SearchVideoPanel extends StatelessWidget {
@ -94,7 +95,7 @@ class SearchVideoPanel extends StatelessWidget {
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero), padding: MaterialStateProperty.all(EdgeInsets.zero),
), ),
onPressed: () => controller.onShowFilterDialog(ctr), onPressed: () => controller.onShowFilterSheet(ctr),
icon: Icon( icon: Icon(
Icons.filter_list_outlined, Icons.filter_list_outlined,
size: 18, size: 18,
@ -165,7 +166,33 @@ class VideoPanelController extends GetxController {
{'label': '30-60分钟', 'value': 3}, {'label': '30-60分钟', 'value': 3},
{'label': '60分钟+', 'value': 4}, {'label': '60分钟+', 'value': 4},
]; ];
List<Map<String, dynamic>> partFiltersList = [
{'label': '全部', 'value': -1},
{'label': '动画', 'value': 1},
{'label': '番剧', 'value': 13},
{'label': '国创', 'value': 167},
{'label': '音乐', 'value': 3},
{'label': '舞蹈', 'value': 129},
{'label': '游戏', 'value': 4},
{'label': '知识', 'value': 36},
{'label': '科技', 'value': 188},
{'label': '运动', 'value': 234},
{'label': '汽车', 'value': 223},
{'label': '生活', 'value': 160},
{'label': '美食', 'value': 211},
{'label': '动物', 'value': 217},
{'label': '鬼畜', 'value': 119},
{'label': '时尚', 'value': 155},
{'label': '资讯', 'value': 202},
{'label': '娱乐', 'value': 5},
{'label': '影视', 'value': 181},
{'label': '记录', 'value': 177},
{'label': '电影', 'value': 23},
{'label': '电视', 'value': 11},
];
RxInt currentTimeFilterval = 0.obs; RxInt currentTimeFilterval = 0.obs;
RxInt currentPartFilterval = (-1).obs;
@override @override
void onInit() { void onInit() {
@ -219,4 +246,100 @@ class VideoPanelController extends GetxController {
}, },
); );
} }
onShowFilterSheet(searchPanelCtr) {
showModalBottomSheet(
context: Get.context!,
builder: (context) {
return StatefulBuilder(
builder: (context, StateSetter setState) {
return Container(
color: Theme.of(Get.context!).colorScheme.surface,
padding: const EdgeInsets.only(top: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
title: Text('内容时长'),
),
Padding(
padding: const EdgeInsets.only(
left: 14,
right: 14,
bottom: 14,
),
child: Wrap(
spacing: 10,
runSpacing: 10,
direction: Axis.horizontal,
textDirection: TextDirection.ltr,
children: [
for (var i in timeFiltersList)
Obx(
() => SearchText(
searchText: i['label'],
searchTextIdx: i['value'],
isSelect:
currentTimeFilterval.value == i['value'],
onSelect: (value) async {
currentTimeFilterval.value = i['value'];
setState(() {});
SmartDialog.showToast("${i['label']}」的筛选结果");
SearchPanelController ctr =
Get.find<SearchPanelController>(
tag: 'video${searchPanelCtr.keyword!}');
ctr.duration.value = i['value'];
Get.back();
SmartDialog.showLoading(msg: '获取中');
await ctr.onRefresh();
SmartDialog.dismiss();
},
onLongSelect: (value) => {},
),
)
],
),
),
const ListTile(
title: Text('内容分区'),
),
Padding(
padding: const EdgeInsets.only(left: 14, right: 14),
child: Wrap(
spacing: 10,
runSpacing: 10,
direction: Axis.horizontal,
textDirection: TextDirection.ltr,
children: [
for (var i in partFiltersList)
SearchText(
searchText: i['label'],
searchTextIdx: i['value'],
isSelect: currentPartFilterval.value == i['value'],
onSelect: (value) async {
currentPartFilterval.value = i['value'];
setState(() {});
SmartDialog.showToast("${i['label']}」的筛选结果");
SearchPanelController ctr =
Get.find<SearchPanelController>(
tag: 'video${searchPanelCtr.keyword!}');
ctr.tids.value = i['value'];
Get.back();
SmartDialog.showLoading(msg: '获取中');
await ctr.onRefresh();
SmartDialog.dismiss();
},
onLongSelect: (value) => {},
)
],
),
)
],
),
);
},
);
},
);
}
} }

View File

@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/common/action_type.dart';
import 'package:pilipala/utils/global_data.dart';
import '../../../utils/storage.dart';
class ActionMenuSetPage extends StatefulWidget {
const ActionMenuSetPage({super.key});
@override
State<ActionMenuSetPage> createState() => _ActionMenuSetPageState();
}
class _ActionMenuSetPageState extends State<ActionMenuSetPage> {
Box setting = GStrorage.setting;
late List<String> actionTypeSort;
late List<Map> allLabels;
@override
void initState() {
super.initState();
actionTypeSort = setting.get(SettingBoxKey.actionTypeSort,
defaultValue: ['like', 'coin', 'collect', 'watchLater', 'share']);
allLabels = actionMenuConfig;
allLabels.sort((a, b) {
int indexA = actionTypeSort.indexOf((a['value'] as ActionType).value);
int indexB = actionTypeSort.indexOf((b['value'] as ActionType).value);
if (indexA == -1) indexA = actionTypeSort.length;
if (indexB == -1) indexB = actionTypeSort.length;
return indexA.compareTo(indexB);
});
}
void saveEdit() {
List<String> sortedTabbar = allLabels
.where((i) => actionTypeSort.contains((i['value'] as ActionType).value))
.map<String>((i) => (i['value'] as ActionType).value)
.toList();
setting.put(SettingBoxKey.actionTypeSort, sortedTabbar);
GlobalData().actionTypeSort = sortedTabbar;
SmartDialog.showToast('操作成功');
}
void onReorder(int oldIndex, int newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final tabsItem = allLabels.removeAt(oldIndex);
allLabels.insert(newIndex, tabsItem);
});
}
@override
Widget build(BuildContext context) {
final listTiles = [
for (int i = 0; i < allLabels.length; i++) ...[
CheckboxListTile(
key: Key((allLabels[i]['value'] as ActionType).value),
value: actionTypeSort
.contains((allLabels[i]['value'] as ActionType).value),
onChanged: (bool? newValue) {
String actionTypeId = (allLabels[i]['value'] as ActionType).value;
if (!newValue!) {
actionTypeSort.remove(actionTypeId);
} else {
actionTypeSort.add(actionTypeId);
}
setState(() {});
},
title: Row(
children: [
allLabels[i]['icon'],
const SizedBox(width: 8),
Text(allLabels[i]['label']),
],
),
secondary: const Icon(Icons.drag_indicator_rounded),
)
]
];
return Scaffold(
appBar: AppBar(
title: const Text('视频操作菜单'),
actions: [
TextButton(onPressed: () => saveEdit(), child: const Text('保存')),
const SizedBox(width: 12)
],
),
body: ReorderableListView(
onReorder: onReorder,
physics: const NeverScrollableScrollPhysics(),
footer: SizedBox(
height: MediaQuery.of(context).padding.bottom + 30,
),
children: listTiles,
),
);
}
}

View File

@ -66,7 +66,7 @@ class _FontSizeSelectPageState extends State<FontSizeSelectPage> {
.colorScheme .colorScheme
.primary .primary
.withOpacity(0.3))), .withOpacity(0.3))),
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.surface,
), ),
child: Row( child: Row(
children: [ children: [

View File

@ -41,7 +41,8 @@ class _LogsPageState extends State<LogsPage> {
.replaceAll('DEVICE INFO', '设备信息') .replaceAll('DEVICE INFO', '设备信息')
.replaceAll('APP INFO', '应用信息') .replaceAll('APP INFO', '应用信息')
.replaceAll('ERROR', '错误信息') .replaceAll('ERROR', '错误信息')
.replaceAll('STACK TRACE', '错误堆栈'); .replaceAll('STACK TRACE', '错误堆栈')
.replaceAll('#', 'Line');
}).toList(); }).toList();
List<Map<String, dynamic>> result = []; List<Map<String, dynamic>> result = [];
for (String i in contentList) { for (String i in contentList) {
@ -50,7 +51,7 @@ class _LogsPageState extends State<LogsPage> {
.split("\n") .split("\n")
.map((l) { .map((l) {
if (l.startsWith("Crash occurred on")) { if (l.startsWith("Crash occurred on")) {
date = DateTime.parse( date = DateTime.tryParse(
l.split("Crash occurred on")[1].trim().split('.')[0], l.split("Crash occurred on")[1].trim().split('.')[0],
); );
return ""; return "";

View File

@ -289,6 +289,11 @@ class _StyleSettingState extends State<StyleSetting> {
onTap: () => Get.toNamed('/navbarSetting'), onTap: () => Get.toNamed('/navbarSetting'),
title: Text('底部导航栏设置', style: titleStyle), title: Text('底部导航栏设置', style: titleStyle),
), ),
// ListTile(
// dense: false,
// onTap: () => Get.toNamed('/actionMenuSet'),
// title: Text('操作菜单设置', style: titleStyle),
// ),
if (Platform.isAndroid) if (Platform.isAndroid)
ListTile( ListTile(
dense: false, dense: false,

View File

@ -53,18 +53,25 @@ class _SubPageState extends State<SubPage> {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
Map? data = snapshot.data; Map? data = snapshot.data;
if (data != null && data['status']) { if (data != null && data['status']) {
return Obx( if (_subController.subFolderData.value.list!.isNotEmpty) {
() => ListView.builder( return Obx(
controller: scrollController, () => ListView.builder(
itemCount: _subController.subFolderData.value.list!.length, controller: scrollController,
itemBuilder: (context, index) { itemCount: _subController.subFolderData.value.list!.length,
return SubItem( itemBuilder: (context, index) {
subFolderItem: return SubItem(
_subController.subFolderData.value.list![index], subFolderItem:
cancelSub: _subController.cancelSub); _subController.subFolderData.value.list![index],
}, cancelSub: _subController.cancelSub);
), },
); ),
);
} else {
return const CustomScrollView(
physics: NeverScrollableScrollPhysics(),
slivers: [HttpError(errMsg: '', btnText: '没有数据', fn: null)],
);
}
} else { } else {
return CustomScrollView( return CustomScrollView(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),

View File

@ -114,16 +114,15 @@ class VideoContent extends StatelessWidget {
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
SizedBox( IconButton(
height: 35, style: ButtonStyle(
width: 35, padding: MaterialStateProperty.all(EdgeInsets.zero),
child: IconButton( ),
onPressed: () => cancelSub?.call(subFolderItem), onPressed: () => cancelSub?.call(subFolderItem),
style: TextButton.styleFrom( icon: Icon(
foregroundColor: Theme.of(context).colorScheme.outline, Icons.clear_outlined,
padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), color: Theme.of(context).colorScheme.outline,
), size: 18,
icon: const Icon(Icons.delete_outline, size: 18),
), ),
) )
], ],

View File

@ -38,6 +38,8 @@ class VideoIntroController extends GetxController {
RxBool hasCoin = false.obs; RxBool hasCoin = false.obs;
// 是否收藏 // 是否收藏
RxBool hasFav = false.obs; RxBool hasFav = false.obs;
// 是否不喜欢
RxBool hasDisLike = false.obs;
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStrorage.userInfo;
bool userLogin = false; bool userLogin = false;
Rx<FavFolderData> favFolderData = FavFolderData().obs; Rx<FavFolderData> favFolderData = FavFolderData().obs;
@ -153,36 +155,16 @@ class VideoIntroController extends GetxController {
SmartDialog.showToast('🙏 UP已经收到了'); SmartDialog.showToast('🙏 UP已经收到了');
return false; return false;
} }
SmartDialog.show( var result = await VideoHttp.oneThree(bvid: bvid);
useSystem: true, print('🤣🦴:${result["data"]}');
animationType: SmartAnimationType.centerFade_otherSlide, if (result['status']) {
builder: (BuildContext context) { hasLike.value = result["data"]["like"];
return AlertDialog( hasCoin.value = result["data"]["coin"];
title: const Text('提示'), hasFav.value = result["data"]["fav"];
content: const Text('一键三连 给UP送温暖'), SmartDialog.showToast('三连成功');
actions: [ } else {
TextButton( SmartDialog.showToast(result['msg']);
onPressed: () => SmartDialog.dismiss(), }
child: const Text('点错了')),
TextButton(
onPressed: () async {
var result = await VideoHttp.oneThree(bvid: bvid);
if (result['status']) {
hasLike.value = result["data"]["like"];
hasCoin.value = result["data"]["coin"];
hasFav.value = result["data"]["fav"];
SmartDialog.showToast('三连成功 🎉');
} else {
SmartDialog.showToast(result['msg']);
}
SmartDialog.dismiss();
},
child: const Text('确认'),
)
],
);
},
);
} }
// (取消)点赞 // (取消)点赞
@ -193,9 +175,8 @@ class VideoIntroController extends GetxController {
} }
var result = await VideoHttp.likeVideo(bvid: bvid, type: !hasLike.value); var result = await VideoHttp.likeVideo(bvid: bvid, type: !hasLike.value);
if (result['status']) { if (result['status']) {
// hasLike.value = result["data"] == 1 ? true : false;
if (!hasLike.value) { if (!hasLike.value) {
SmartDialog.showToast('点赞成功 👍'); SmartDialog.showToast('点赞成功');
hasLike.value = true; hasLike.value = true;
videoDetail.value.stat!.like = videoDetail.value.stat!.like! + 1; videoDetail.value.stat!.like = videoDetail.value.stat!.like! + 1;
} else if (hasLike.value) { } else if (hasLike.value) {
@ -215,6 +196,10 @@ class VideoIntroController extends GetxController {
SmartDialog.showToast('账号未登录'); SmartDialog.showToast('账号未登录');
return; return;
} }
if (hasCoin.value) {
SmartDialog.showToast('已投过币了');
return;
}
showDialog( showDialog(
context: Get.context!, context: Get.context!,
builder: (context) { builder: (context) {
@ -236,7 +221,7 @@ class VideoIntroController extends GetxController {
var res = await VideoHttp.coinVideo( var res = await VideoHttp.coinVideo(
bvid: bvid, multiply: _tempThemeValue); bvid: bvid, multiply: _tempThemeValue);
if (res['status']) { if (res['status']) {
SmartDialog.showToast('投币成功 👏'); SmartDialog.showToast('投币成功');
hasCoin.value = true; hasCoin.value = true;
videoDetail.value.stat!.coin = videoDetail.value.stat!.coin =
videoDetail.value.stat!.coin! + _tempThemeValue; videoDetail.value.stat!.coin! + _tempThemeValue;
@ -269,7 +254,7 @@ class VideoIntroController extends GetxController {
if (result['status']) { if (result['status']) {
// 重新获取收藏状态 // 重新获取收藏状态
await queryHasFavVideo(); await queryHasFavVideo();
SmartDialog.showToast('操作成功'); SmartDialog.showToast('操作成功');
} else { } else {
SmartDialog.showToast(result['msg']); SmartDialog.showToast(result['msg']);
} }
@ -299,7 +284,7 @@ class VideoIntroController extends GetxController {
Get.back(); Get.back();
// 重新获取收藏状态 // 重新获取收藏状态
await queryHasFavVideo(); await queryHasFavVideo();
SmartDialog.showToast('操作成功'); SmartDialog.showToast('操作成功');
} else { } else {
SmartDialog.showToast(result['msg']); SmartDialog.showToast(result['msg']);
} }

View File

@ -1,10 +1,11 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:expandable/expandable.dart'; import 'package:expandable/expandable.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:lottie/lottie.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
@ -15,6 +16,7 @@ import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/introduction/controller.dart'; import 'package:pilipala/pages/video/detail/introduction/controller.dart';
import 'package:pilipala/pages/video/detail/widgets/ai_detail.dart'; import 'package:pilipala/pages/video/detail/widgets/ai_detail.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/global_data.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import '../../../../http/user.dart'; import '../../../../http/user.dart';
@ -97,11 +99,14 @@ class _VideoIntroPanelState extends State<VideoIntroPanel>
); );
} }
} else { } else {
return const SliverToBoxAdapter( return SliverToBoxAdapter(
child: SizedBox( child: SizedBox(
height: 100, height: 100,
child: Center( child: Center(
child: CircularProgressIndicator(), child: Lottie.asset(
'assets/loading.json',
width: 200,
),
), ),
), ),
); );
@ -143,13 +148,18 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
RxBool isExpand = false.obs; RxBool isExpand = false.obs;
late ExpandableController _expandableCtr; late ExpandableController _expandableCtr;
void Function()? handleState(Future Function() action) { // 一键三连动画
late AnimationController _controller;
late Animation<double> _scaleTransition;
final RxDouble _progress = 0.0.obs;
void Function()? handleState(Future<dynamic> Function() action) {
return isProcessing return isProcessing
? null ? null
: () async { : () async {
setState(() => isProcessing = true); isProcessing = true;
await action(); await action.call();
setState(() => isProcessing = false); isProcessing = false;
}; };
} }
@ -166,6 +176,26 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
follower = Utils.numFormat(videoIntroController.userStat['follower']); follower = Utils.numFormat(videoIntroController.userStat['follower']);
enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true); enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true);
_expandableCtr = ExpandableController(initialExpanded: false); _expandableCtr = ExpandableController(initialExpanded: false);
/// 一键三连动画
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
reverseDuration: const Duration(milliseconds: 300),
vsync: this,
);
_scaleTransition = Tween<double>(begin: 0.5, end: 1.5).animate(_controller)
..addListener(() async {
_progress.value =
double.parse((_scaleTransition.value - 0.5).toStringAsFixed(3));
if (_progress.value == 1) {
if (_controller.status == AnimationStatus.completed) {
await videoIntroController.actionOneThree();
}
_progress.value = 0;
_scaleTransition.removeListener(() {});
_controller.stop();
}
});
} }
// 收藏 // 收藏
@ -246,6 +276,8 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
@override @override
void dispose() { void dispose() {
_expandableCtr.dispose(); _expandableCtr.dispose();
_controller.dispose();
_scaleTransition.removeListener(() {});
super.dispose(); super.dispose();
} }
@ -535,68 +567,183 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
} }
Widget actionGrid(BuildContext context, videoIntroController) { Widget actionGrid(BuildContext context, videoIntroController) {
final actionTypeSort = GlobalData().actionTypeSort;
Widget progressWidget(progress) {
return SizedBox(
width: const IconThemeData.fallback().size! + 5,
height: const IconThemeData.fallback().size! + 5,
child: CircularProgressIndicator(
value: progress.value,
strokeWidth: 2,
),
);
}
Map<String, Widget> menuListWidgets = {
'like': Obx(
() {
bool likeStatus = videoIntroController.hasLike.value;
ColorScheme colorScheme = Theme.of(context).colorScheme;
return Stack(
children: [
Positioned(
top: ((Get.size.width - 24) / 5) / 2 -
(const IconThemeData.fallback().size!),
left: ((Get.size.width - 24) / 5) / 2 -
(const IconThemeData.fallback().size! + 5) / 2,
child: progressWidget(_progress)),
InkWell(
onTapDown: (details) {
feedBack();
if (videoIntroController.userInfo == null) {
SmartDialog.showToast('账号未登录');
return;
}
_controller.forward();
},
onTapUp: (TapUpDetails details) {
if (_progress.value == 0) {
feedBack();
EasyThrottle.throttle(
'my-throttler', const Duration(milliseconds: 200), () {
videoIntroController.actionLikeVideo();
});
}
_controller.reverse();
},
borderRadius: StyleString.mdRadius,
child: SizedBox(
width: (Get.size.width - 24) / 5,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 4),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder:
(Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation, child: child);
},
child: Icon(
key: ValueKey<bool>(likeStatus),
likeStatus
? Icons.thumb_up
: Icons.thumb_up_alt_outlined,
color: likeStatus
? colorScheme.primary
: colorScheme.outline,
),
),
const SizedBox(height: 6),
Text(
widget.videoDetail!.stat!.like!.toString(),
style: TextStyle(
color: likeStatus ? colorScheme.primary : null,
fontSize:
Theme.of(context).textTheme.labelSmall!.fontSize,
),
)
],
),
),
),
],
);
},
),
'coin': Obx(
() => Stack(
children: [
Positioned(
top: ((Get.size.width - 24) / 5) / 2 -
(const IconThemeData.fallback().size!),
left: ((Get.size.width - 24) / 5) / 2 -
(const IconThemeData.fallback().size! + 5) / 2,
child: progressWidget(_progress)),
ActionItem(
icon: Image.asset('assets/images/coin.png', width: 30),
onTap: handleState(videoIntroController.actionCoinVideo),
selectStatus: videoIntroController.hasCoin.value,
text: widget.videoDetail!.stat!.coin!.toString(),
),
],
),
),
'collect': Obx(
() => Stack(
children: [
Positioned(
top: ((Get.size.width - 24) / 5) / 2 -
(const IconThemeData.fallback().size!),
left: ((Get.size.width - 24) / 5) / 2 -
(const IconThemeData.fallback().size! + 5) / 2,
child: progressWidget(_progress)),
ActionItem(
icon: const Icon(Icons.star_border),
selectIcon: const Icon(Icons.star),
onTap: () => showFavBottomSheet(),
onLongPress: () => showFavBottomSheet(type: 'longPress'),
selectStatus: videoIntroController.hasFav.value,
text: widget.videoDetail!.stat!.favorite!.toString(),
),
],
),
),
'watchLater': ActionItem(
icon: const Icon(Icons.watch_later_outlined),
onTap: () async {
final res =
await UserHttp.toViewLater(bvid: widget.videoDetail!.bvid);
SmartDialog.showToast(res['msg']);
},
selectStatus: false,
text: '稍后看',
),
'share': ActionItem(
icon: const Icon(Icons.share),
onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false,
text: '分享',
),
'dislike': Obx(
() => ActionItem(
icon: const Icon(Icons.thumb_down_alt_outlined),
selectIcon: const Icon(Icons.thumb_down),
onTap: () {},
selectStatus: videoIntroController.hasDisLike.value,
text: '不喜欢',
),
),
'downloadCover': ActionItem(
icon: const Icon(Icons.image_outlined),
onTap: () {},
selectStatus: false,
text: '下载封面',
),
'copyLink': ActionItem(
icon: const Icon(Icons.link_outlined),
onTap: () {},
selectStatus: false,
text: '复制链接',
),
};
final List<Widget> list = [];
for (var i = 0; i < actionTypeSort.length; i++) {
list.add(menuListWidgets[actionTypeSort[i]]!);
}
return LayoutBuilder( return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) { builder: (BuildContext context, BoxConstraints constraints) {
return Container( return Container(
margin: const EdgeInsets.only(top: 6, bottom: 4), margin: const EdgeInsets.only(top: 6, bottom: 4),
height: constraints.maxWidth / 5 * 0.8, height: constraints.maxWidth / 5,
child: GridView.count( child: ListView(
physics: const NeverScrollableScrollPhysics(), scrollDirection: Axis.horizontal,
primary: false, children: list,
padding: EdgeInsets.zero,
crossAxisCount: 5,
childAspectRatio: 1.25,
children: <Widget>[
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.thumbsUp),
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: handleState(videoIntroController.actionLikeVideo),
selectStatus: videoIntroController.hasLike.value,
text: widget.videoDetail!.stat!.like!.toString()),
),
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.b),
selectIcon: const Icon(FontAwesomeIcons.b),
onTap: handleState(videoIntroController.actionCoinVideo),
selectStatus: videoIntroController.hasCoin.value,
text: widget.videoDetail!.stat!.coin!.toString(),
),
),
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.star),
selectIcon: const Icon(FontAwesomeIcons.solidStar),
onTap: () => showFavBottomSheet(),
onLongPress: () => showFavBottomSheet(type: 'longPress'),
selectStatus: videoIntroController.hasFav.value,
text: widget.videoDetail!.stat!.favorite!.toString(),
),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.clock),
onTap: () async {
final res =
await UserHttp.toViewLater(bvid: widget.videoDetail!.bvid);
SmartDialog.showToast(res['msg']);
},
selectStatus: false,
text: '稍后看',
),
ActionItem(
icon: const Icon(FontAwesomeIcons.shareFromSquare),
onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false,
text: '分享',
),
],
), ),
); );
}); });
} }
// Widget StaffPanel(BuildContext context, videoIntroController) {
// return
// }
} }

View File

@ -1,9 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
class ActionItem extends StatelessWidget { class ActionItem extends StatelessWidget {
final Icon? icon; final dynamic icon;
final Icon? selectIcon; final Icon? selectIcon;
final Function? onTap; final Function? onTap;
final Function? onLongPress; final Function? onLongPress;
@ -31,26 +32,46 @@ class ActionItem extends StatelessWidget {
if (onLongPress != null) {onLongPress!()} if (onLongPress != null) {onLongPress!()}
}, },
borderRadius: StyleString.mdRadius, borderRadius: StyleString.mdRadius,
child: Column( child: SizedBox(
mainAxisAlignment: MainAxisAlignment.center, width: (Get.size.width - 24) / 5,
children: [ child: Column(
const SizedBox(height: 4), mainAxisAlignment: MainAxisAlignment.center,
selectStatus children: [
? Icon(selectIcon!.icon!, const SizedBox(height: 4),
size: 18, color: Theme.of(context).colorScheme.primary) AnimatedSwitcher(
: Icon(icon!.icon!, duration: const Duration(milliseconds: 300),
size: 18, color: Theme.of(context).colorScheme.outline), transitionBuilder: (Widget child, Animation<double> animation) {
const SizedBox(height: 6), return ScaleTransition(scale: animation, child: child);
Text( },
text ?? '', child: icon is Icon
style: TextStyle( ? Icon(
color: selectStatus selectStatus
? Theme.of(context).colorScheme.primary ? selectIcon!.icon ?? icon!.icon
: Theme.of(context).colorScheme.outline, : icon!.icon,
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize, color: selectStatus
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
)
: Image.asset(
key: ValueKey<bool>(selectStatus),
'assets/images/coin.png',
width: const IconThemeData.fallback().size,
color: selectStatus
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
),
), ),
) const SizedBox(height: 6),
], Text(
text ?? '',
style: TextStyle(
color:
selectStatus ? Theme.of(context).colorScheme.primary : null,
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
),
),
],
),
), ),
); );
} }

View File

@ -29,7 +29,7 @@ class _FavPanelState extends State<FavPanel> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
height: sheetHeight, height: sheetHeight,
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.surface,
child: Column( child: Column(
children: [ children: [
AppBar( AppBar(

View File

@ -57,7 +57,7 @@ class _GroupPanelState extends State<GroupPanel> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
height: sheetHeight, height: sheetHeight,
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.surface,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
AppBar( AppBar(

View File

@ -12,7 +12,7 @@ class MenuRow extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
width: double.infinity, width: double.infinity,
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.surface,
padding: const EdgeInsets.only(top: 9, bottom: 9, left: 12), padding: const EdgeInsets.only(top: 9, bottom: 9, left: 12),
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@ -84,7 +84,7 @@ class MenuRow extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: selectStatus color: selectStatus
? Theme.of(context).colorScheme.onBackground ? Theme.of(context).colorScheme.onSurface
: Theme.of(context).colorScheme.outline), : Theme.of(context).colorScheme.outline),
), ),
), ),

View File

@ -97,7 +97,7 @@ class ReplyItem extends StatelessWidget {
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(7), borderRadius: BorderRadius.circular(7),
color: colorScheme.background, color: colorScheme.surface,
), ),
child: Icon( child: Icon(
Icons.offline_bolt, Icons.offline_bolt,

View File

@ -117,6 +117,7 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
final String newText = currentText.substring(0, cursorPosition) + final String newText = currentText.substring(0, cursorPosition) +
emote.text! + emote.text! +
currentText.substring(cursorPosition); currentText.substring(cursorPosition);
message.value = newText;
_replyContentController.value = TextEditingValue( _replyContentController.value = TextEditingValue(
text: newText, text: newText,
selection: selection:
@ -170,7 +171,7 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
topLeft: Radius.circular(12), topLeft: Radius.circular(12),
topRight: Radius.circular(12), topRight: Radius.circular(12),
), ),
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.surface,
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -214,7 +215,15 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
), ),
Container( Container(
height: 52, height: 52,
padding: const EdgeInsets.only(left: 12, right: 12), padding: const EdgeInsets.only(
left: 12,
right: 12,
),
margin: EdgeInsets.only(
bottom: toolbarType == 'input' && keyboardHeight == 0.0
? MediaQuery.of(context).padding.bottom
: 0,
),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [

View File

@ -78,7 +78,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
height: widget.source == 'videoDetail' ? widget.sheetHeight : null, height: widget.source == 'videoDetail' ? widget.sheetHeight : null,
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.surface,
child: Column( child: Column(
children: [ children: [
if (widget.source == 'videoDetail') if (widget.source == 'videoDetail')

View File

@ -514,6 +514,13 @@ class _VideoDetailPageState extends State<VideoDetailPage>
showEposideCb: () => vdCtr.videoType == SearchType.video showEposideCb: () => vdCtr.videoType == SearchType.video
? videoIntroController.showEposideHandler() ? videoIntroController.showEposideHandler()
: bangumiIntroController.showEposideHandler(), : bangumiIntroController.showEposideHandler(),
fullScreenCb: (bool status) {
if (status) {
videoHeight.value = Get.size.height;
} else {
videoHeight.value = defaultVideoHeight;
}
},
), ),
); );
}, },
@ -591,27 +598,32 @@ class _VideoDetailPageState extends State<VideoDetailPage>
verticalScreen(); verticalScreen();
} }
}, },
child: Hero( child: LayoutBuilder(
tag: heroTag, builder: (BuildContext context,
child: Stack( BoxConstraints constraints) {
children: <Widget>[ return Hero(
if (isShowing) videoPlayerPanel, tag: heroTag,
child: Stack(
children: <Widget>[
if (isShowing) videoPlayerPanel,
/// 关闭自动播放时 手动播放 /// 关闭自动播放时 手动播放
Obx( Obx(
() => Visibility( () => Visibility(
visible: !vdCtr.autoPlay.value && visible: !vdCtr.autoPlay.value &&
vdCtr.isShowCover.value, vdCtr.isShowCover.value,
child: Positioned( child: Positioned(
top: 0, top: 0,
left: 0, left: 0,
right: 0, right: 0,
child: handlePlayPanel(), child: handlePlayPanel(),
),
),
), ),
), ],
), ),
], );
), },
), ),
), ),
), ),

View File

@ -23,7 +23,7 @@ class AiDetail extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
sheetHeight = localCache.get('sheetHeight'); sheetHeight = localCache.get('sheetHeight');
return Container( return Container(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.surface,
padding: const EdgeInsets.only(left: 14, right: 14), padding: const EdgeInsets.only(left: 14, right: 14),
height: sheetHeight, height: sheetHeight,
child: Column( child: Column(
@ -108,7 +108,7 @@ class AiDetail extends StatelessWidget {
fontSize: 13, fontSize: 13,
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.onBackground, .onSurface,
height: 1.5, height: 1.5,
), ),
children: [ children: [

View File

@ -29,7 +29,7 @@ class ScrollAppBar extends StatelessWidget {
opacity: scrollDistance / (videoHeight - kToolbarHeight), opacity: scrollDistance / (videoHeight - kToolbarHeight),
child: Container( child: Container(
height: statusBarHeight + kToolbarHeight, height: statusBarHeight + kToolbarHeight,
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.surface,
padding: EdgeInsets.only(top: statusBarHeight), padding: EdgeInsets.only(top: statusBarHeight),
child: AppBar( child: AppBar(
primary: false, primary: false,

View File

@ -93,7 +93,7 @@ class _HeaderControlState extends State<HeaderControl> {
height: 460, height: 460,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.all(Radius.circular(12)), borderRadius: const BorderRadius.all(Radius.circular(12)),
), ),
margin: const EdgeInsets.all(12), margin: const EdgeInsets.all(12),
@ -317,7 +317,7 @@ class _HeaderControlState extends State<HeaderControl> {
height: 500, height: 500,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.all(Radius.circular(12)), borderRadius: const BorderRadius.all(Radius.circular(12)),
), ),
margin: const EdgeInsets.all(12), margin: const EdgeInsets.all(12),
@ -377,7 +377,7 @@ class _HeaderControlState extends State<HeaderControl> {
inactiveThumbColor: inactiveThumbColor:
Theme.of(context).colorScheme.primaryContainer, Theme.of(context).colorScheme.primaryContainer,
inactiveTrackColor: inactiveTrackColor:
Theme.of(context).colorScheme.background, Theme.of(context).colorScheme.surface,
splashRadius: 10.0, splashRadius: 10.0,
// boolean variable value // boolean variable value
value: shutdownTimerService.waitForPlayingCompleted, value: shutdownTimerService.waitForPlayingCompleted,
@ -570,7 +570,7 @@ class _HeaderControlState extends State<HeaderControl> {
height: 310, height: 310,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.all(Radius.circular(12)), borderRadius: const BorderRadius.all(Radius.circular(12)),
), ),
margin: const EdgeInsets.all(12), margin: const EdgeInsets.all(12),
@ -660,7 +660,7 @@ class _HeaderControlState extends State<HeaderControl> {
height: 250, height: 250,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.all(Radius.circular(12)), borderRadius: const BorderRadius.all(Radius.circular(12)),
), ),
margin: const EdgeInsets.all(12), margin: const EdgeInsets.all(12),
@ -734,7 +734,7 @@ class _HeaderControlState extends State<HeaderControl> {
height: 250, height: 250,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.all(Radius.circular(12)), borderRadius: const BorderRadius.all(Radius.circular(12)),
), ),
margin: const EdgeInsets.all(12), margin: const EdgeInsets.all(12),
@ -828,7 +828,7 @@ class _HeaderControlState extends State<HeaderControl> {
height: 580, height: 580,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.all(Radius.circular(12)), borderRadius: const BorderRadius.all(Radius.circular(12)),
), ),
margin: const EdgeInsets.all(12), margin: const EdgeInsets.all(12),
@ -1069,7 +1069,9 @@ class _HeaderControlState extends State<HeaderControl> {
); );
}); });
}, },
); ).then((value) {
widget.controller!.cacheDanmakuOption();
});
} }
/// 播放顺序 /// 播放顺序
@ -1084,7 +1086,7 @@ class _HeaderControlState extends State<HeaderControl> {
height: 250, height: 250,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.all(Radius.circular(12)), borderRadius: const BorderRadius.all(Radius.circular(12)),
), ),
margin: const EdgeInsets.all(12), margin: const EdgeInsets.all(12),

View File

@ -76,7 +76,7 @@ class WebviewController extends GetxController {
(url.startsWith( (url.startsWith(
'https://passport.bilibili.com/web/sso/exchange_cookie') || 'https://passport.bilibili.com/web/sso/exchange_cookie') ||
url.startsWith('https://m.bilibili.com/'))) { url.startsWith('https://m.bilibili.com/'))) {
confirmLogin(url); LoginUtils.confirmLogin(url, controller);
} }
}, },
onWebResourceError: (WebResourceError error) {}, onWebResourceError: (WebResourceError error) {},
@ -97,54 +97,4 @@ class WebviewController extends GetxController {
) )
..loadRequest(Uri.parse(url)); ..loadRequest(Uri.parse(url));
} }
confirmLogin(url) async {
var content = '';
if (url != null) {
content = '${content + url}; \n';
}
try {
await SetCookie.onSet();
final result = await UserHttp.userInfo();
if (result['status'] && result['data'].isLogin) {
SmartDialog.showToast('登录成功');
try {
Box userInfoCache = GStrorage.userInfo;
if (!userInfoCache.isOpen) {
userInfoCache = await Hive.openBox('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

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/utils/login.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'controller.dart'; import 'controller.dart';
import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter/webview_flutter.dart';
@ -43,7 +44,8 @@ class _WebviewPageState extends State<WebviewPage> {
Obx( Obx(
() => _webviewController.type.value == 'login' () => _webviewController.type.value == 'login'
? TextButton( ? TextButton(
onPressed: () => _webviewController.confirmLogin(null), onPressed: () =>
LoginUtils.confirmLogin(null, _webviewController),
child: const Text('刷新登录状态'), child: const Text('刷新登录状态'),
) )
: const SizedBox(), : const SizedBox(),

View File

@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/http/msg.dart'; import 'package:pilipala/http/msg.dart';
import 'package:pilipala/models/msg/account.dart'; import 'package:pilipala/models/msg/account.dart';
@ -7,6 +8,38 @@ class WhisperController extends GetxController {
RxList<SessionList> sessionList = <SessionList>[].obs; RxList<SessionList> sessionList = <SessionList>[].obs;
RxList<AccountListModel> accountList = <AccountListModel>[].obs; RxList<AccountListModel> accountList = <AccountListModel>[].obs;
bool isLoading = false; bool isLoading = false;
RxList noticesList = [
{
'icon': Icons.message_outlined,
'title': '回复我的',
'path': '/messageReply',
'count': 0,
},
{
'icon': Icons.alternate_email,
'title': '@ 我的',
'path': '/messageAt',
'count': 0,
},
{
'icon': Icons.thumb_up_outlined,
'title': '收到的赞',
'path': '/messageLike',
'count': 0,
},
{
'icon': Icons.notifications_none_outlined,
'title': '系统通知',
'path': '/messageSystem',
'count': 0,
}
].obs;
@override
void onInit() {
unread();
super.onInit();
}
Future querySessionList(String? type) async { Future querySessionList(String? type) async {
if (isLoading) return; if (isLoading) return;
@ -62,4 +95,31 @@ class WhisperController extends GetxController {
Future onRefresh() async { Future onRefresh() async {
querySessionList('onRefresh'); querySessionList('onRefresh');
} }
void refreshLastMsg(int talkerId, String content) {
final SessionList currentItem =
sessionList.where((p0) => p0.talkerId == talkerId).first;
currentItem.lastMsg!.content['content'] = content;
sessionList.removeWhere((p0) => p0.talkerId == talkerId);
sessionList.insert(0, currentItem);
sessionList.refresh();
}
// 移除会话
void removeSessionMsg(int talkerId) {
sessionList.removeWhere((p0) => p0.talkerId == talkerId);
sessionList.refresh();
}
// 消息未读数
void unread() async {
var res = await MsgHttp.unread();
if (res['status']) {
noticesList[0]['count'] = res['data']['reply'];
noticesList[1]['count'] = res['data']['at'];
noticesList[2]['count'] = res['data']['like'];
noticesList[3]['count'] = res['data']['sys_msg'];
noticesList.refresh();
}
}
} }

View File

@ -1,6 +1,8 @@
import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/skeleton/skeleton.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
@ -43,198 +45,218 @@ class _WhisperPageState extends State<WhisperPage> {
appBar: AppBar( appBar: AppBar(
title: const Text('消息'), title: const Text('消息'),
), ),
body: Column( body: RefreshIndicator(
children: [ onRefresh: () async {
// LayoutBuilder( _whisperController.unread();
// builder: (BuildContext context, BoxConstraints constraints) { await _whisperController.onRefresh();
// // 在这里根据父级容器的约束条件构建小部件树 },
// return Padding( child: SingleChildScrollView(
// padding: const EdgeInsets.only(left: 20, right: 20), controller: _scrollController,
// child: SizedBox( child: Column(
// height: constraints.maxWidth / 5, children: [
// child: GridView.count( LayoutBuilder(
// primary: false, builder: (BuildContext context, BoxConstraints constraints) {
// crossAxisCount: 4, // 在这里根据父级容器的约束条件构建小部件树
// padding: const EdgeInsets.all(0), return Padding(
// childAspectRatio: 1.25, padding: const EdgeInsets.only(left: 20, right: 20),
// children: [ child: SizedBox(
// Column( height: constraints.maxWidth / 4,
// crossAxisAlignment: CrossAxisAlignment.center, child: Obx(
// mainAxisAlignment: MainAxisAlignment.center, () => GridView.count(
// children: [ primary: false,
// SizedBox( crossAxisCount: 4,
// width: 36, padding: const EdgeInsets.all(0),
// height: 36, children: [
// child: IconButton( ..._whisperController.noticesList.map((element) {
// style: ButtonStyle( return InkWell(
// padding: onTap: () => Get.toNamed(element['path']),
// MaterialStateProperty.all(EdgeInsets.zero), onLongPress: () {},
// backgroundColor: borderRadius: StyleString.mdRadius,
// MaterialStateProperty.resolveWith((states) { child: Column(
// return Theme.of(context) mainAxisAlignment: MainAxisAlignment.center,
// .colorScheme children: [
// .primary Badge(
// .withOpacity(0.1); isLabelVisible: element['count'] > 0,
// }), label: Text(element['count'] > 99
// ), ? '99+'
// onPressed: () {}, : element['count'].toString()),
// icon: Icon( child: Padding(
// Icons.message_outlined, padding: const EdgeInsets.all(10),
// size: 18, child: Icon(
// color: Theme.of(context).colorScheme.primary, element['icon'],
// ), size: 21,
// ), color: Theme.of(context)
// ), .colorScheme
// const SizedBox(height: 6), .primary,
// const Text('回复我的', style: TextStyle(fontSize: 13)) ),
// ], ),
// ),
// ],
// ),
// ),
// );
// },
// ),
Expanded(
child: RefreshIndicator(
onRefresh: () async {
await _whisperController.onRefresh();
},
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: [
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map? data = snapshot.data;
if (data != null && data['status']) {
RxList sessionList = _whisperController.sessionList;
return Obx(
() => sessionList.isEmpty
? const SizedBox()
: ListView.separated(
itemCount: sessionList.length,
shrinkWrap: true,
physics:
const NeverScrollableScrollPhysics(),
itemBuilder: (_, int i) {
return ListTile(
onTap: () {
sessionList[i].unreadCount = 0;
sessionList.refresh();
Get.toNamed(
'/whisperDetail',
parameters: {
'talkerId': sessionList[i]
.talkerId
.toString(),
'name': sessionList[i]
.accountInfo
.name,
'face': sessionList[i]
.accountInfo
.face,
'mid': sessionList[i]
.accountInfo
.mid
.toString(),
},
);
},
leading: Badge(
isLabelVisible:
sessionList[i].unreadCount > 0,
label: Text(sessionList[i]
.unreadCount
.toString()),
alignment: Alignment.topRight,
child: NetworkImgLayer(
width: 45,
height: 45,
type: 'avatar',
src: sessionList[i]
.accountInfo
.face,
),
),
title: Text(
sessionList[i].accountInfo.name),
subtitle: Text(
sessionList[i].lastMsg.content !=
null &&
sessionList[i]
.lastMsg
.content !=
''
? (sessionList[i]
.lastMsg
.content['text'] ??
sessionList[i]
.lastMsg
.content['content'] ??
sessionList[i]
.lastMsg
.content['title'] ??
sessionList[i]
.lastMsg
.content[
'reply_content'] ??
'不支持的消息类型')
: '不支持的消息类型',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.labelMedium!
.copyWith(
color: Theme.of(context)
.colorScheme
.outline)),
trailing: Text(
Utils.dateFormat(sessionList[i]
.lastMsg
.timestamp),
style: Theme.of(context)
.textTheme
.labelSmall!
.copyWith(
color: Theme.of(context)
.colorScheme
.outline),
),
);
},
separatorBuilder:
(BuildContext context, int index) {
return Divider(
indent: 72,
endIndent: 20,
height: 6,
color: Colors.grey.withOpacity(0.1),
);
},
), ),
); const SizedBox(height: 4),
} else { Text(element['title'])
// 请求错误 ],
return Center( ),
child: Text(data?['msg'] ?? '请求异常'), );
); }).toList(),
} ],
} else { ),
// 骨架屏 ),
return const SizedBox(); ),
} );
}, },
)
],
),
), ),
), FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map? data = snapshot.data;
if (data != null && data['status']) {
RxList sessionList = _whisperController.sessionList;
return Obx(
() => sessionList.isEmpty
? const SizedBox()
: ListView.separated(
itemCount: sessionList.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (_, int i) {
return SessionItem(
sessionItem: sessionList[i],
changeFucCall: () => sessionList.refresh(),
);
},
separatorBuilder:
(BuildContext context, int index) {
return Divider(
indent: 72,
endIndent: 20,
height: 6,
color: Colors.grey.withOpacity(0.1),
);
},
),
);
} else {
// 请求错误
return Center(
child: Text(data?['msg'] ?? '请求异常'),
);
}
} else {
// 骨架屏
return ListView.builder(
itemCount: 15,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, int i) {
return Skeleton(
child: ListTile(
leading: Container(
width: 45,
height: 45,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.onInverseSurface,
borderRadius: BorderRadius.circular(25),
),
),
title: Container(
width: 100,
height: 14,
color: Theme.of(context)
.colorScheme
.onInverseSurface,
),
subtitle: Container(
width: 80,
height: 14,
color: Theme.of(context)
.colorScheme
.onInverseSurface,
),
),
);
},
);
}
},
),
],
), ),
], ),
),
);
}
}
class SessionItem extends StatelessWidget {
final dynamic sessionItem;
final Function changeFucCall;
const SessionItem({
super.key,
required this.sessionItem,
required this.changeFucCall,
});
@override
Widget build(BuildContext context) {
final String heroTag = Utils.makeHeroTag(sessionItem.accountInfo.mid);
final content = sessionItem.lastMsg.content;
final msgStatus = sessionItem.lastMsg.msgStatus;
return ListTile(
onTap: () {
sessionItem.unreadCount = 0;
changeFucCall.call();
Get.toNamed(
'/whisperDetail',
parameters: {
'talkerId': sessionItem.talkerId.toString(),
'name': sessionItem.accountInfo.name,
'face': sessionItem.accountInfo.face,
'mid': sessionItem.accountInfo.mid.toString(),
'heroTag': heroTag,
},
);
},
leading: Badge(
isLabelVisible: sessionItem.unreadCount > 0,
label: Text(sessionItem.unreadCount.toString()),
alignment: Alignment.topRight,
child: Hero(
tag: heroTag,
child: NetworkImgLayer(
width: 45,
height: 45,
type: 'avatar',
src: sessionItem.accountInfo.face,
),
),
),
title: Text(sessionItem.accountInfo.name),
subtitle: Text(
msgStatus == 1
? '你撤回了一条消息'
: content != null && content != ''
? (content['text'] ??
content['content'] ??
content['title'] ??
content['reply_content'] ??
'不支持的消息类型')
: '不支持的消息类型',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.labelMedium!
.copyWith(color: Theme.of(context).colorScheme.outline)),
trailing: Text(
Utils.dateFormat(sessionItem.lastMsg.timestamp),
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
), ),
); );
} }

View File

@ -1,30 +1,40 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/http/msg.dart'; import 'package:pilipala/http/msg.dart';
import 'package:pilipala/models/msg/session.dart'; import 'package:pilipala/models/msg/session.dart';
import 'package:pilipala/pages/whisper/index.dart';
import '../../utils/feed_back.dart'; import '../../utils/feed_back.dart';
import '../../utils/storage.dart'; import '../../utils/storage.dart';
class WhisperDetailController extends GetxController { class WhisperDetailController extends GetxController {
late int talkerId; int? talkerId;
late String name; late String name;
late String face; late String face;
late String mid; late String mid;
late String heroTag;
RxList<MessageItem> messageList = <MessageItem>[].obs; RxList<MessageItem> messageList = <MessageItem>[].obs;
//表情转换图片规则 //表情转换图片规则
List<dynamic>? eInfos; RxList<dynamic> eInfos = [].obs;
final TextEditingController replyContentController = TextEditingController(); final TextEditingController replyContentController = TextEditingController();
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStrorage.userInfo;
List emoteList = [];
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
talkerId = int.parse(Get.parameters['talkerId']!); if (Get.parameters.containsKey('talkerId')) {
talkerId = int.parse(Get.parameters['talkerId']!);
} else {
talkerId = int.parse(Get.parameters['mid']!);
}
name = Get.parameters['name']!; name = Get.parameters['name']!;
face = Get.parameters['face']!; face = Get.parameters['face']!;
mid = Get.parameters['mid']!; mid = Get.parameters['mid']!;
heroTag = Get.parameters['heroTag']!;
} }
Future querySessionMsg() async { Future querySessionMsg() async {
@ -34,7 +44,7 @@ class WhisperDetailController extends GetxController {
if (messageList.isNotEmpty) { if (messageList.isNotEmpty) {
ackSessionMsg(); ackSessionMsg();
if (res['data'].eInfos != null) { if (res['data'].eInfos != null) {
eInfos = res['data'].eInfos; eInfos.value = res['data'].eInfos;
} }
} }
} else { } else {
@ -73,9 +83,64 @@ class WhisperDetailController extends GetxController {
msgType: 1, msgType: 1,
); );
if (result['status']) { if (result['status']) {
SmartDialog.showToast('发送成功'); String content = jsonDecode(result['data']['msg_content'])['content'];
messageList.insert(
0,
MessageItem(
msgSeqno: result['data']['msg_key'],
senderUid: userInfo.mid,
receiverId: int.parse(mid),
content: {'content': content},
msgType: 1,
timestamp: DateTime.now().millisecondsSinceEpoch,
),
);
eInfos.addAll(emoteList);
replyContentController.clear();
try {
late final WhisperController whisperController =
Get.find<WhisperController>();
whisperController.refreshLastMsg(talkerId!, message);
} catch (_) {}
} else { } else {
SmartDialog.showToast(result['msg']); SmartDialog.showToast(result['msg']);
} }
} }
void removeSession(context) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
clipBehavior: Clip.hardEdge,
title: const Text('提示'),
content: const Text('确认清空会话内容并移除会话?'),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
TextButton(
onPressed: () async {
var res = await MsgHttp.removeSession(talkerId: talkerId);
if (res['status']) {
SmartDialog.showToast('操作成功');
try {
late final WhisperController whisperController =
Get.find<WhisperController>();
whisperController.removeSessionMsg(talkerId!);
Get.back();
} catch (_) {}
}
},
child: const Text('确认'),
),
],
);
},
);
}
} }

View File

@ -1,9 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/video/reply/emote.dart';
import 'package:pilipala/pages/emote/index.dart'; import 'package:pilipala/pages/emote/index.dart';
import 'package:pilipala/pages/video/detail/reply_new/toolbar_icon_button.dart';
import 'package:pilipala/pages/whisper_detail/controller.dart'; import 'package:pilipala/pages/whisper_detail/controller.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import '../../utils/storage.dart'; import '../../utils/storage.dart';
@ -24,9 +27,9 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
late TextEditingController _replyContentController; late TextEditingController _replyContentController;
final FocusNode replyContentFocusNode = FocusNode(); final FocusNode replyContentFocusNode = FocusNode();
final _debouncer = Debouncer(milliseconds: 200); // 设置延迟时间 final _debouncer = Debouncer(milliseconds: 200); // 设置延迟时间
late double emoteHeight = 0.0; late double emoteHeight = 230.0;
double keyboardHeight = 0.0; // 键盘高度 double keyboardHeight = 0.0; // 键盘高度
String toolbarType = 'input'; RxString toolbarType = ''.obs;
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStrorage.userInfo;
@override @override
@ -41,9 +44,7 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
_focuslistener() { _focuslistener() {
replyContentFocusNode.addListener(() { replyContentFocusNode.addListener(() {
if (replyContentFocusNode.hasFocus) { if (replyContentFocusNode.hasFocus) {
setState(() { toolbarType.value = 'input';
toolbarType = 'input';
});
} }
}); });
} }
@ -52,7 +53,7 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
void didChangeMetrics() { void didChangeMetrics() {
super.didChangeMetrics(); super.didChangeMetrics();
final String routePath = Get.currentRoute; final String routePath = Get.currentRoute;
if (mounted && routePath.startsWith('/whisper_detail')) { if (mounted && routePath.startsWith('/whisperDetail')) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
// 键盘高度 // 键盘高度
final viewInsets = EdgeInsets.fromViewPadding( final viewInsets = EdgeInsets.fromViewPadding(
@ -61,8 +62,11 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
if (mounted) { if (mounted) {
if (keyboardHeight == 0) { if (keyboardHeight == 0) {
setState(() { setState(() {
emoteHeight = keyboardHeight = keyboardHeight =
keyboardHeight == 0.0 ? viewInsets.bottom : keyboardHeight; keyboardHeight == 0.0 ? viewInsets.bottom : keyboardHeight;
if (keyboardHeight != 0) {
emoteHeight = keyboardHeight;
}
}); });
} }
} }
@ -79,6 +83,23 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
super.dispose(); super.dispose();
} }
void onChooseEmote(PackageItem package, Emote emote) {
_whisperDetailController.emoteList.add(
{'text': emote.text, 'url': emote.url},
);
final int cursorPosition =
max(_replyContentController.selection.baseOffset, 0);
final String currentText = _replyContentController.text;
final String newText = currentText.substring(0, cursorPosition) +
emote.text! +
currentText.substring(cursorPosition);
_replyContentController.value = TextEditingValue(
text: newText,
selection:
TextSelection.collapsed(offset: cursorPosition + emote.text!.length),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -88,30 +109,20 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
width: double.infinity, width: double.infinity,
height: 50, height: 50,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
SizedBox( SizedBox(
width: 34, width: 34,
height: 34, height: 34,
child: IconButton( child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
backgroundColor: MaterialStateProperty.resolveWith(
(Set<MaterialState> states) {
return Theme.of(context)
.colorScheme
.primaryContainer
.withOpacity(0.6);
}),
),
onPressed: () => Get.back(), onPressed: () => Get.back(),
icon: Icon( icon: Icon(
Icons.arrow_back_outlined, Icons.arrow_back_ios,
size: 18, size: 18,
color: Theme.of(context).colorScheme.onPrimaryContainer, color: Theme.of(context).colorScheme.onPrimaryContainer,
), ),
), ),
), ),
const SizedBox(width: 10),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
feedBack(); feedBack();
@ -125,13 +136,16 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
}, },
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
NetworkImgLayer( Hero(
width: 34, tag: _whisperDetailController.heroTag,
height: 34, child: NetworkImgLayer(
type: 'avatar', width: 34,
src: _whisperDetailController.face, height: 34,
type: 'avatar',
src: _whisperDetailController.face,
),
), ),
const SizedBox(width: 6), const SizedBox(width: 10),
Text( Text(
_whisperDetailController.name, _whisperDetailController.name,
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
@ -143,155 +157,171 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
], ],
), ),
), ),
actions: [
PopupMenuButton(
icon: const Icon(Icons.more_vert_outlined, size: 20),
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
PopupMenuItem(
onTap: () => _whisperDetailController.removeSession(context),
child: const Text('关闭会话'),
)
],
),
const SizedBox(width: 14)
],
), ),
body: GestureDetector( body: Column(
onTap: () { children: [
FocusScope.of(context).unfocus(); Expanded(
setState(() { child: GestureDetector(
keyboardHeight = 0; onTap: () {
}); FocusScope.of(context).unfocus();
}, toolbarType.value = '';
child: FutureBuilder( },
future: _futureBuilderFuture, child: FutureBuilder(
builder: (BuildContext context, snapshot) { future: _futureBuilderFuture,
if (snapshot.connectionState == ConnectionState.done) { builder: (BuildContext context, snapshot) {
if (snapshot.data == null) { if (snapshot.connectionState == ConnectionState.done) {
return const SizedBox(); if (snapshot.data == null) {
} return const SizedBox();
final Map data = snapshot.data as Map; }
if (data['status']) { final Map data = snapshot.data as Map;
List messageList = _whisperDetailController.messageList; if (data['status']) {
return Obx( List messageList = _whisperDetailController.messageList;
() => messageList.isEmpty return Obx(
? const SizedBox() () => messageList.isEmpty
: ListView.builder( ? const SizedBox()
itemCount: messageList.length, : Align(
shrinkWrap: true, alignment: Alignment.topCenter,
reverse: true, child: ListView.builder(
itemBuilder: (_, int i) { itemCount: messageList.length,
if (i == 0) { shrinkWrap: true,
return Column( reverse: true,
children: [ itemBuilder: (_, int i) {
ChatItem( if (i == 0) {
item: messageList[i], return Column(
e_infos: _whisperDetailController.eInfos), children: [
const SizedBox(height: 12), ChatItem(
], item: messageList[i],
); e_infos: _whisperDetailController
} else { .eInfos),
return ChatItem( const SizedBox(height: 20),
item: messageList[i], ],
e_infos: _whisperDetailController.eInfos); );
} } else {
}, return ChatItem(
), item: messageList[i],
); e_infos:
} else { _whisperDetailController.eInfos);
// 请求错误 }
return const SizedBox(); },
} ),
} else { ),
// 骨架屏 );
return const SizedBox(); } else {
} // 请求错误
}, return const SizedBox();
), }
), } else {
// resizeToAvoidBottomInset: true, // 骨架屏
bottomNavigationBar: Container( return const SizedBox();
width: double.infinity, }
height: MediaQuery.of(context).padding.bottom + 70 + keyboardHeight, },
padding: EdgeInsets.only( ),
left: 8,
right: 12,
top: 12,
bottom: MediaQuery.of(context).padding.bottom,
),
decoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 4,
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
), ),
), ),
), Obx(
child: Column( () => Container(
children: [ padding: EdgeInsets.fromLTRB(
Row( 8,
mainAxisAlignment: MainAxisAlignment.center, 12,
crossAxisAlignment: CrossAxisAlignment.start, 12,
children: [ toolbarType.value == ''
// IconButton( ? MediaQuery.of(context).padding.bottom + 6
// onPressed: () {}, : 6,
// icon: Icon( ),
// Icons.add_circle_outline, decoration: BoxDecoration(
// color: Theme.of(context).colorScheme.outline, border: Border(
// ), top: BorderSide(
// ), width: 1,
IconButton( color: Colors.grey.withOpacity(0.15),
onPressed: () {
// if (toolbarType == 'input') {
// setState(() {
// toolbarType = 'emote';
// });
// }
// FocusScope.of(context).unfocus();
},
icon: Icon(
Icons.emoji_emotions_outlined,
color: Theme.of(context).colorScheme.outline,
), ),
), ),
Expanded( ),
child: Container( child: Row(
height: 45, mainAxisAlignment: MainAxisAlignment.center,
decoration: BoxDecoration( children: [
color: Theme.of(context) ToolbarIconButton(
.colorScheme onPressed: () {
.primary if (toolbarType.value == '') {
.withOpacity(0.08), toolbarType.value = 'emote';
borderRadius: BorderRadius.circular(40.0), } else if (toolbarType.value == 'input') {
), FocusScope.of(context).unfocus();
child: TextField( toolbarType.value = 'emote';
readOnly: true, } else if (toolbarType.value == 'emote') {
style: Theme.of(context).textTheme.titleMedium, FocusScope.of(context).requestFocus();
controller: _replyContentController, }
autofocus: false, },
focusNode: replyContentFocusNode, icon: const Icon(Icons.emoji_emotions_outlined, size: 22),
decoration: const InputDecoration( toolbarType: toolbarType.value,
border: InputBorder.none, // 移除默认边框 selected: false,
hintText: '开发中 ...', // 提示文本 ),
contentPadding: EdgeInsets.symmetric( const SizedBox(width: 4),
horizontal: 16.0, vertical: 12.0), // 内边距 Expanded(
child: Container(
height: 45,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.outline
.withOpacity(0.05),
borderRadius: BorderRadius.circular(40.0),
),
child: TextField(
style: Theme.of(context).textTheme.titleMedium,
controller: _replyContentController,
autofocus: false,
focusNode: replyContentFocusNode,
decoration: const InputDecoration(
border: InputBorder.none, // 移除默认边框
hintText: '文明发言 ', // 提示文本
contentPadding: EdgeInsets.symmetric(
horizontal: 16.0, vertical: 12.0), // 内边距
),
), ),
), ),
), ),
), IconButton(
IconButton( onPressed: _whisperDetailController.sendMsg,
// onPressed: _whisperDetailController.sendMsg, icon: Icon(
onPressed: null, Icons.send,
icon: Icon( color: Theme.of(context).colorScheme.outline,
Icons.send, ),
color: Theme.of(context).colorScheme.outline,
), ),
), ],
// const SizedBox(width: 16), ),
],
), ),
AnimatedSize( ),
curve: Curves.easeInOut, Obx(
duration: const Duration(milliseconds: 300), () => AnimatedSize(
curve: Curves.linear,
duration: const Duration(milliseconds: 200),
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
height: toolbarType == 'input' ? keyboardHeight : emoteHeight, height: toolbarType.value == 'input'
? keyboardHeight
: toolbarType.value == 'emote'
? emoteHeight
: 0,
child: EmotePanel( child: EmotePanel(
onChoose: (package, emote) => {}, onChoose: (package, emote) => onChooseEmote(package, emote),
), ),
), ),
), ),
], ),
), ],
), ),
resizeToAvoidBottomInset: false,
); );
} }
} }

View File

@ -1,7 +1,6 @@
// ignore_for_file: must_be_immutable // ignore_for_file: must_be_immutable
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -69,9 +68,13 @@ class ChatItem extends StatelessWidget {
Color textColor(BuildContext context) { Color textColor(BuildContext context) {
return isOwner return isOwner
? Theme.of(context).colorScheme.onPrimary ? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.onSecondaryContainer; : Theme.of(context).colorScheme.onBackground;
} }
const double safeDistanceval = 6;
const double borderRadiusVal = 12;
const double paddingVal = 10;
Widget richTextMessage(BuildContext context) { Widget richTextMessage(BuildContext context) {
var text = content['content']; var text = content['content'];
if (e_infos != null) { if (e_infos != null) {
@ -259,115 +262,114 @@ class ChatItem extends StatelessWidget {
); );
case MsgType.auto_reply_push: case MsgType.auto_reply_push:
return Container( return Container(
constraints: const BoxConstraints( constraints: const BoxConstraints(
maxWidth: 300.0, // 设置最大宽度为200.0 maxWidth: 300.0, // 设置最大宽度为200.0
),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.secondaryContainer
.withOpacity(0.4),
borderRadius: const BorderRadius.all(
Radius.circular(16),
), ),
decoration: BoxDecoration( ),
color: Theme.of(context) margin: const EdgeInsets.all(12),
.colorScheme padding: const EdgeInsets.all(12),
.secondaryContainer child: Column(
.withOpacity(0.4), crossAxisAlignment: CrossAxisAlignment.start,
borderRadius: const BorderRadius.only( children: [
topLeft: Radius.circular(16), Text(
topRight: Radius.circular(16), content['main_title'],
bottomLeft: Radius.circular(6), style: TextStyle(
bottomRight: Radius.circular(16), letterSpacing: 0.6,
), height: 1.5,
), color: textColor(context),
margin: const EdgeInsets.all(12), fontWeight: FontWeight.bold,
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
content['main_title'],
style: TextStyle(
letterSpacing: 0.6,
height: 1.5,
color: textColor(context),
fontWeight: FontWeight.bold,
),
), ),
for (var i in content['sub_cards']) ...<Widget>[ ),
const SizedBox(height: 6), for (var i in content['sub_cards']) ...<Widget>[
GestureDetector( const SizedBox(height: 6),
onTap: () async { GestureDetector(
RegExp bvRegex = RegExp(r'BV[0-9A-Za-z]{10}', onTap: () async {
caseSensitive: false); RegExp bvRegex =
Iterable<Match> matches = RegExp(r'BV[0-9A-Za-z]{10}', caseSensitive: false);
bvRegex.allMatches(i['jump_url']); Iterable<Match> matches =
if (matches.isNotEmpty) { bvRegex.allMatches(i['jump_url']);
Match match = matches.first; if (matches.isNotEmpty) {
String bvid = match.group(0)!; Match match = matches.first;
try { String bvid = match.group(0)!;
SmartDialog.showLoading(); try {
final int cid = await SearchHttp.ab2c(bvid: bvid); SmartDialog.showLoading();
final String heroTag = Utils.makeHeroTag(bvid); final int cid = await SearchHttp.ab2c(bvid: bvid);
SmartDialog.dismiss<dynamic>().then( final String heroTag = Utils.makeHeroTag(bvid);
(e) => Get.toNamed<dynamic>( SmartDialog.dismiss<dynamic>().then(
'/video?bvid=$bvid&cid=$cid', (e) => Get.toNamed<dynamic>(
arguments: <String, String?>{ '/video?bvid=$bvid&cid=$cid',
'pic': i['cover_url'], arguments: <String, String?>{
'heroTag': heroTag, 'pic': i['cover_url'],
}), 'heroTag': heroTag,
); }),
} catch (err) { );
SmartDialog.dismiss(); } catch (err) {
SmartDialog.showToast(err.toString()); SmartDialog.dismiss();
} SmartDialog.showToast(err.toString());
} else { }
SmartDialog.showToast('未匹配到 BV 号'); } else {
Get.toNamed('/webview', SmartDialog.showToast('未匹配到 BV 号');
arguments: {'url': i['jump_url']}); Get.toNamed('/webview',
} arguments: {'url': i['jump_url']});
}, }
child: Row( },
child: Row(
children: [
NetworkImgLayer(
width: 130,
height: 130 * 9 / 16,
src: i['cover_url'],
),
const SizedBox(width: 6),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
NetworkImgLayer( Text(
width: 130, i['field1'],
height: 130 * 9 / 16, maxLines: 2,
src: i['cover_url'], style: TextStyle(
letterSpacing: 0.6,
height: 1.5,
color: textColor(context),
fontWeight: FontWeight.bold,
),
),
Text(
i['field2'],
style: TextStyle(
letterSpacing: 0.6,
height: 1.5,
color: textColor(context).withOpacity(0.6),
fontSize: 12,
),
),
Text(
i['field3'],
style: TextStyle(
letterSpacing: 0.6,
height: 1.5,
color: textColor(context).withOpacity(0.6),
fontSize: 12,
),
), ),
const SizedBox(width: 6),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
i['field1'],
maxLines: 2,
style: TextStyle(
letterSpacing: 0.6,
height: 1.5,
color: textColor(context),
fontWeight: FontWeight.bold,
),
),
Text(
i['field2'],
style: TextStyle(
letterSpacing: 0.6,
height: 1.5,
color: textColor(context).withOpacity(0.6),
fontSize: 12,
),
),
Text(
Utils.timeFormat(int.parse(i['field3'])),
style: TextStyle(
letterSpacing: 0.6,
height: 1.5,
color: textColor(context).withOpacity(0.6),
fontSize: 12,
),
),
],
)),
], ],
)), )),
], ],
),
),
], ],
)); ],
),
);
default: default:
return Text( return Text(
content != null && content != '' content != null && content != ''
@ -387,73 +389,109 @@ class ChatItem extends StatelessWidget {
? messageContent(context) ? messageContent(context)
: isRevoke : isRevoke
? const SizedBox() ? const SizedBox()
: Row( : Container(
children: [ padding: const EdgeInsets.only(top: 6, bottom: 6),
if (!isOwner) const SizedBox(width: 12), decoration: BoxDecoration(
if (isOwner) const Spacer(), border: Border(
Container( left: item.msgStatus == 1 && !isOwner
constraints: const BoxConstraints( ? BorderSide(
maxWidth: 300.0, // 设置最大宽度为200.0 width: 4, color: Theme.of(context).dividerColor)
), : BorderSide.none,
decoration: BoxDecoration( right: item.msgStatus == 1 && isOwner
color: isOwner ? BorderSide(
? Theme.of(context).colorScheme.primary width: 4, color: Theme.of(context).primaryColor)
: Theme.of(context).colorScheme.secondaryContainer, : BorderSide.none,
borderRadius: BorderRadius.only( )),
topLeft: const Radius.circular(16), child: Row(
topRight: const Radius.circular(16), mainAxisAlignment: !isOwner
bottomLeft: Radius.circular(isOwner ? 16 : 6), ? MainAxisAlignment.start
bottomRight: Radius.circular(isOwner ? 6 : 16), : MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(width: safeDistanceval),
if (isOwner)
Text(
Utils.dateFormat(item.timestamp),
style: Theme.of(context).textTheme.labelSmall!.copyWith(
color: Theme.of(context).colorScheme.outline),
), ),
Container(
constraints: const BoxConstraints(
maxWidth: 300.0, // 设置最大宽度为200.0
),
decoration: BoxDecoration(
color: isOwner
? Theme.of(context)
.colorScheme
.primary
.withAlpha(180)
: Theme.of(context)
.colorScheme
.outlineVariant
.withOpacity(0.6)
.withAlpha(125),
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(borderRadiusVal),
topRight: const Radius.circular(borderRadiusVal),
bottomLeft:
Radius.circular(isOwner ? borderRadiusVal : 2),
bottomRight:
Radius.circular(isOwner ? 2 : borderRadiusVal),
),
),
margin: const EdgeInsets.only(
left: 8,
right: 8,
),
padding: const EdgeInsets.all(paddingVal),
child: messageContent(context),
// child: Column(
// crossAxisAlignment: isOwner
// ? CrossAxisAlignment.end
// : CrossAxisAlignment.start,
// children: [
// messageContent(context),
// SizedBox(height: isPic ? 7 : 2),
// Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// Text(
// Utils.dateFormat(item.timestamp),
// style: Theme.of(context)
// .textTheme
// .labelSmall!
// .copyWith(
// color: isOwner
// ? Theme.of(context)
// .colorScheme
// .onPrimary
// .withOpacity(0.8)
// : Theme.of(context)
// .colorScheme
// .onSecondaryContainer
// .withOpacity(0.8)),
// ),
// item.msgStatus == 1
// ? Text(
// ' 已撤回',
// style:
// Theme.of(context).textTheme.labelSmall!,
// )
// : const SizedBox()
// ],
// )
// ],
// ),
), ),
margin: const EdgeInsets.only(top: 12), if (!isOwner)
padding: EdgeInsets.only( Text(
top: 8, Utils.dateFormat(item.timestamp),
bottom: 6, style: Theme.of(context).textTheme.labelSmall!.copyWith(
left: isPic ? 8 : 12, color: Theme.of(context).colorScheme.outline),
right: isPic ? 8 : 12, ),
), const SizedBox(width: safeDistanceval),
child: Column( ],
crossAxisAlignment: isOwner ),
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
messageContent(context),
SizedBox(height: isPic ? 7 : 2),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
Utils.dateFormat(item.timestamp),
style: Theme.of(context)
.textTheme
.labelSmall!
.copyWith(
color: isOwner
? Theme.of(context)
.colorScheme
.onPrimary
.withOpacity(0.8)
: Theme.of(context)
.colorScheme
.onSecondaryContainer
.withOpacity(0.8)),
),
item.msgStatus == 1
? Text(
' 已撤回',
style:
Theme.of(context).textTheme.labelSmall!,
)
: const SizedBox()
],
)
],
),
),
if (!isOwner) const Spacer(),
if (isOwner) const SizedBox(width: 12),
],
); );
} }
} }

View File

@ -1089,6 +1089,16 @@ class PlPlayerController {
videoStorage.put(VideoBoxKey.playRepeat, type.value); videoStorage.put(VideoBoxKey.playRepeat, type.value);
} }
/// 缓存本次弹幕选项
cacheDanmakuOption() {
localCache.put(LocalCacheKey.danmakuBlockType, blockTypes);
localCache.put(LocalCacheKey.danmakuShowArea, showArea);
localCache.put(LocalCacheKey.danmakuOpacity, opacityVal);
localCache.put(LocalCacheKey.danmakuFontScale, fontSizeVal);
localCache.put(LocalCacheKey.danmakuDuration, danmakuDurationVal);
localCache.put(LocalCacheKey.strokeWidth, strokeWidth);
}
Future<void> dispose({String type = 'single'}) async { Future<void> dispose({String type = 'single'}) async {
// 每次减1最后销毁 // 每次减1最后销毁
if (type == 'single' && playerCount.value > 1) { if (type == 'single' && playerCount.value > 1) {
@ -1118,12 +1128,7 @@ class PlPlayerController {
// dataStatus.status.close(); // dataStatus.status.close();
/// 缓存本次弹幕选项 /// 缓存本次弹幕选项
localCache.put(LocalCacheKey.danmakuBlockType, blockTypes); cacheDanmakuOption();
localCache.put(LocalCacheKey.danmakuShowArea, showArea);
localCache.put(LocalCacheKey.danmakuOpacity, opacityVal);
localCache.put(LocalCacheKey.danmakuFontScale, fontSizeVal);
localCache.put(LocalCacheKey.danmakuDuration, danmakuDurationVal);
localCache.put(LocalCacheKey.strokeWidth, strokeWidth);
if (_videoPlayerController != null) { if (_videoPlayerController != null) {
var pp = _videoPlayerController!.platform as NativePlayer; var pp = _videoPlayerController!.platform as NativePlayer;
await pp.setProperty('audio-files', ''); await pp.setProperty('audio-files', '');

View File

@ -7,6 +7,7 @@ import 'package:flutter_volume_controller/flutter_volume_controller.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:lottie/lottie.dart';
import 'package:media_kit/media_kit.dart'; import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart'; import 'package:media_kit_video/media_kit_video.dart';
import 'package:pilipala/models/common/gesture_mode.dart'; import 'package:pilipala/models/common/gesture_mode.dart';
@ -38,6 +39,7 @@ class PLVideoPlayer extends StatefulWidget {
this.customWidget, this.customWidget,
this.customWidgets, this.customWidgets,
this.showEposideCb, this.showEposideCb,
this.fullScreenCb,
super.key, super.key,
}); });
@ -51,6 +53,7 @@ class PLVideoPlayer extends StatefulWidget {
final Widget? customWidget; final Widget? customWidget;
final List<Widget>? customWidgets; final List<Widget>? customWidgets;
final Function? showEposideCb; final Function? showEposideCb;
final Function? fullScreenCb;
@override @override
State<PLVideoPlayer> createState() => _PLVideoPlayerState(); State<PLVideoPlayer> createState() => _PLVideoPlayerState();
@ -334,7 +337,10 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
color: Colors.white, color: Colors.white,
), ),
), ),
fuc: () => _.triggerFullScreen(status: !_.isFullScreen.value), fuc: () {
_.triggerFullScreen(status: !_.isFullScreen.value);
widget.fullScreenCb?.call(!_.isFullScreen.value);
},
), ),
}; };
final List<Widget> list = []; final List<Widget> list = [];
@ -913,9 +919,9 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
colors: [Colors.black26, Colors.transparent], colors: [Colors.black26, Colors.transparent],
), ),
), ),
child: Image.asset( child: Lottie.asset(
'assets/images/loading.gif', 'assets/loading.json',
height: 25, width: 200,
), ),
), ),
); );
@ -939,7 +945,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
begin: 0.0, begin: 0.0,
end: _hideSeekBackwardButton.value ? 0.0 : 1.0, end: _hideSeekBackwardButton.value ? 0.0 : 1.0,
), ),
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 150),
builder: (BuildContext context, double value, builder: (BuildContext context, double value,
Widget? child) => Widget? child) =>
Opacity( Opacity(
@ -982,7 +988,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
begin: 0.0, begin: 0.0,
end: _hideSeekForwardButton.value ? 0.0 : 1.0, end: _hideSeekForwardButton.value ? 0.0 : 1.0,
), ),
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 150),
builder: (BuildContext context, double value, builder: (BuildContext context, double value,
Widget? child) => Widget? child) =>
Opacity( Opacity(

View File

@ -30,14 +30,14 @@ class BackwardSeekIndicatorState extends State<BackwardSeekIndicator> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
timer = Timer(const Duration(milliseconds: 400), () { timer = Timer(const Duration(milliseconds: 200), () {
widget.onSubmitted.call(value); widget.onSubmitted.call(value);
}); });
} }
void increment() { void increment() {
timer?.cancel(); timer?.cancel();
timer = Timer(const Duration(milliseconds: 400), () { timer = Timer(const Duration(milliseconds: 200), () {
widget.onSubmitted.call(value); widget.onSubmitted.call(value);
}); });
widget.onChanged.call(value); widget.onChanged.call(value);

View File

@ -30,14 +30,14 @@ class ForwardSeekIndicatorState extends State<ForwardSeekIndicator> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
timer = Timer(const Duration(milliseconds: 400), () { timer = Timer(const Duration(milliseconds: 200), () {
widget.onSubmitted.call(value); widget.onSubmitted.call(value);
}); });
} }
void increment() { void increment() {
timer?.cancel(); timer?.cancel();
timer = Timer(const Duration(milliseconds: 400), () { timer = Timer(const Duration(milliseconds: 200), () {
widget.onSubmitted.call(value); widget.onSubmitted.call(value);
}); });
widget.onChanged.call(value); widget.onChanged.call(value);

View File

@ -4,6 +4,10 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/pages/follow_search/view.dart'; import 'package:pilipala/pages/follow_search/view.dart';
import 'package:pilipala/pages/message/at/index.dart';
import 'package:pilipala/pages/message/like/index.dart';
import 'package:pilipala/pages/message/reply/index.dart';
import 'package:pilipala/pages/message/system/index.dart';
import 'package:pilipala/pages/setting/pages/logs.dart'; import 'package:pilipala/pages/setting/pages/logs.dart';
import '../pages/about/index.dart'; import '../pages/about/index.dart';
@ -35,6 +39,7 @@ import '../pages/search/index.dart';
import '../pages/search_result/index.dart'; import '../pages/search_result/index.dart';
import '../pages/setting/extra_setting.dart'; import '../pages/setting/extra_setting.dart';
import '../pages/setting/index.dart'; import '../pages/setting/index.dart';
import '../pages/setting/pages/action_menu_set.dart';
import '../pages/setting/pages/color_select.dart'; import '../pages/setting/pages/color_select.dart';
import '../pages/setting/pages/display_mode.dart'; import '../pages/setting/pages/display_mode.dart';
import '../pages/setting/pages/font_size_select.dart'; import '../pages/setting/pages/font_size_select.dart';
@ -174,6 +179,18 @@ class Routes {
// navigation bar // navigation bar
CustomGetPage( CustomGetPage(
name: '/navbarSetting', page: () => const NavigationBarSetPage()), name: '/navbarSetting', page: () => const NavigationBarSetPage()),
// 操作菜单
CustomGetPage(
name: '/actionMenuSet', page: () => const ActionMenuSetPage()),
// 回复我的
CustomGetPage(name: '/messageReply', page: () => const MessageReplyPage()),
// @我的
CustomGetPage(name: '/messageAt', page: () => const MessageAtPage()),
// 收到的赞
CustomGetPage(name: '/messageLike', page: () => const MessageLikePage()),
// 系统通知
CustomGetPage(
name: '/messageSystem', page: () => const MessageSystemPage()),
]; ];
} }

View File

@ -205,9 +205,7 @@ class PiliSchame {
parameters: {'url': redirectUrl, 'type': 'url', 'pageTitle': ''}, parameters: {'url': redirectUrl, 'type': 'url', 'pageTitle': ''},
); );
} }
} } else if (path != null) {
if (path != null) {
final String area = path.split('/').last; final String area = path.split('/').last;
switch (area) { switch (area) {
case 'bangumi': case 'bangumi':

View File

@ -11,7 +11,8 @@ class GlobalData {
bool enablePlayerControlAnimation = true; bool enablePlayerControlAnimation = true;
final bool enableMYBar = final bool enableMYBar =
setting.get(SettingBoxKey.enableMYBar, defaultValue: true); setting.get(SettingBoxKey.enableMYBar, defaultValue: true);
List<String> actionTypeSort = setting.get(SettingBoxKey.actionTypeSort,
defaultValue: ['like', 'coin', 'collect', 'watchLater', 'share']);
// 私有构造函数 // 私有构造函数
GlobalData._(); GlobalData._();

View File

@ -12,7 +12,7 @@ Future imageSaveDialog(context, videoItem, closeFn) {
builder: (context) => Container( builder: (context) => Container(
margin: const EdgeInsets.symmetric(horizontal: StyleString.safeSpace), margin: const EdgeInsets.symmetric(horizontal: StyleString.safeSpace),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(10.0), borderRadius: BorderRadius.circular(10.0),
), ),
child: Column( child: Column(

View File

@ -2,12 +2,18 @@ import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/pages/dynamics/index.dart'; import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/pages/mine/index.dart'; import 'package:pilipala/pages/mine/index.dart';
import 'package:pilipala/utils/cookie.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class LoginUtils { class LoginUtils {
@ -57,4 +63,56 @@ class LoginUtils {
String uuid = getUUID() + getUUID(); String uuid = getUUID() + getUUID();
return 'XY${uuid.substring(0, 35).toUpperCase()}'; return 'XY${uuid.substring(0, 35).toUpperCase()}';
} }
static confirmLogin(url, controller) async {
var content = '';
if (url != null) {
content = '${content + url}; \n';
}
try {
await SetCookie.onSet();
final result = await UserHttp.userInfo();
if (result['status'] && result['data'].isLogin) {
SmartDialog.showToast('登录成功');
try {
Box userInfoCache = GStrorage.userInfo;
if (!userInfoCache.isOpen) {
userInfoCache = await Hive.openBox('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 != null
? () => controller.reload()
: SmartDialog.dismiss,
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

@ -149,7 +149,8 @@ class SettingBoxKey {
tabbarSort = 'tabbarSort', // 首页tabbar tabbarSort = 'tabbarSort', // 首页tabbar
dynamicBadgeMode = 'dynamicBadgeMode', dynamicBadgeMode = 'dynamicBadgeMode',
enableGradientBg = 'enableGradientBg', enableGradientBg = 'enableGradientBg',
navBarSort = 'navBarSort'; navBarSort = 'navBarSort',
actionTypeSort = 'actionTypeSort';
} }
class LocalCacheKey { class LocalCacheKey {

View File

@ -674,10 +674,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: gt3_flutter_plugin name: gt3_flutter_plugin
sha256: f12bff2bfbcf27467833f8d564dcc24ee2f1b3254a7c7cf5eb2c4590baf11cc1 sha256: "08f35692e937770ad6b3e2017eb8ef81839a82b8a63f5acf3abab14b688fc36c"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.0.8" version: "0.1.0"
hive: hive:
dependency: "direct main" dependency: "direct main"
description: description:
@ -762,10 +762,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: intl name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.19.0" version: "0.18.1"
io: io:
dependency: transitive dependency: transitive
description: description:
@ -794,26 +794,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "10.0.4" version: "10.0.0"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_flutter_testing name: leak_tracker_flutter_testing
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.0.3" version: "2.0.1"
leak_tracker_testing: leak_tracker_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_testing name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.0.1" version: "2.0.1"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@ -862,6 +862,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.2.0" version: "1.2.0"
lottie:
dependency: "direct main"
description:
name: lottie
sha256: "6a24ade5d3d918c306bb1c21a6b9a04aab0489d51a2582522eea820b4093b62b"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.2"
mailer: mailer:
dependency: transitive dependency: transitive
description: description:
@ -965,18 +973,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.12.0" version: "1.11.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
name: mime name: mime
sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.0.4" version: "1.0.5"
nm: nm:
dependency: transitive dependency: transitive
description: description:
@ -1046,10 +1054,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: path_provider name: path_provider
sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.1" version: "2.1.3"
path_provider_android: path_provider_android:
dependency: transitive dependency: transitive
description: description:
@ -1062,10 +1070,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_foundation name: path_provider_foundation
sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.3.1" version: "2.4.0"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@ -1210,6 +1218,22 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.1.0" version: "3.1.0"
qr:
dependency: transitive
description:
name: qr
sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.1"
qr_flutter:
dependency: "direct main"
description:
name: qr_flutter
sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.1.0"
rxdart: rxdart:
dependency: transitive dependency: transitive
description: description:
@ -1230,10 +1254,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: saver_gallery name: saver_gallery
sha256: "2657953427ebe5a3b2d08157d41587c01923ccce3f1a616d55082be7470f8530" sha256: "0f740608072053a0da3b19cc5812a87e36f5c3c0b959d2475c4eb3d697f4a782"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.0.1" version: "3.0.3"
screen_brightness: screen_brightness:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1443,10 +1467,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.7.0" version: "0.6.1"
timing: timing:
dependency: transitive dependency: transitive
description: description:
@ -1603,10 +1627,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "14.2.1" version: "13.0.0"
volume_controller: volume_controller:
dependency: transitive dependency: transitive
description: description:
@ -1683,10 +1707,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_android name: webview_flutter_android
sha256: f42447ca49523f11d8f70abea55ea211b3cafe172dd7a0e7ac007bb35dd356dc sha256: "0d21cfc3bfdd2e30ab2ebeced66512b91134b39e72e97b43db2d47dda1c4e53a"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.16.4" version: "3.16.3"
webview_flutter_platform_interface: webview_flutter_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -1744,5 +1768,5 @@ packages:
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
sdks: sdks:
dart: ">=3.4.0 <4.0.0" dart: ">=3.3.0 <4.0.0"
flutter: ">=3.22.0" flutter: ">=3.19.0"

View File

@ -132,7 +132,7 @@ dependencies:
# html渲染 # html渲染
flutter_html: ^3.0.0-beta.2 flutter_html: ^3.0.0-beta.2
# 极验 # 极验
gt3_flutter_plugin: ^0.0.8 gt3_flutter_plugin: ^0.1.0
uuid: ^3.0.7 uuid: ^3.0.7
scrollable_positioned_list: ^0.3.8 scrollable_positioned_list: ^0.3.8
catcher_2: ^1.2.6 catcher_2: ^1.2.6
@ -144,6 +144,9 @@ dependencies:
expandable: ^5.0.1 expandable: ^5.0.1
# 投屏 # 投屏
dlna_dart: ^0.0.8 dlna_dart: ^0.0.8
lottie: ^3.1.2
# 二维码
qr_flutter: ^4.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -203,6 +206,7 @@ flutter:
# To add assets to your application, add an assets section, like this: # To add assets to your application, add an assets section, like this:
assets: assets:
- assets/
- assets/images/ - assets/images/
- assets/images/lv/ - assets/images/lv/
- assets/images/logo/ - assets/images/logo/