diff --git a/lib/http/api.dart b/lib/http/api.dart index e519d91c..f20b8bcf 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -535,4 +535,17 @@ class Api { /// 搜索结果计数 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'; } diff --git a/lib/http/msg.dart b/lib/http/msg.dart index d1d31958..7c168230 100644 --- a/lib/http/msg.dart +++ b/lib/http/msg.dart @@ -1,4 +1,8 @@ +import 'dart:convert'; 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/session.dart'; import '../utils/wbi_sign.dart'; @@ -122,68 +126,48 @@ class MsgHttp { 'data': res.data['data'], }; } else { - return { - 'status': false, - 'date': [], - 'msg': "message: ${res.data['message']}," - " msg: ${res.data['msg']}," - " code: ${res.data['code']}", - }; + return {'status': false, 'date': [], 'msg': res.data['message']}; } } // 发送私信 static Future sendMsg({ - int? senderUid, - int? receiverId, + required int senderUid, + required int receiverId, int? receiverType, int? msgType, dynamic content, }) async { String csrf = await Request.getCsrf(); - Map params = await WbiSign().makSign({ - 'msg[sender_uid]': senderUid, - 'msg[receiver_id]': receiverId, - 'msg[receiver_type]': receiverType ?? 1, - 'msg[msg_type]': msgType ?? 1, - 'msg[msg_status]': 0, - 'msg[dev_id]': getDevId(), - 'msg[timestamp]': DateTime.now().millisecondsSinceEpoch ~/ 1000, - 'msg[new_face_version]': 0, - 'msg[content]': content, - 'from_firework': 0, - 'build': 0, - 'mobi_app': 'web', - 'csrf_token': csrf, - 'csrf': csrf, - }); - var res = - await Request().post(Api.sendMsg, queryParameters: { - ...params, - 'csrf_token': csrf, - '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, - }); + var res = await Request().post( + Api.sendMsg, + data: { + 'msg[sender_uid]': senderUid, + 'msg[receiver_id]': receiverId, + 'msg[receiver_type]': 1, + 'msg[msg_type]': 1, + 'msg[msg_status]': 0, + 'msg[content]': jsonEncode(content), + 'msg[timestamp]': DateTime.now().millisecondsSinceEpoch ~/ 1000, + 'msg[new_face_version]': 0, + 'msg[dev_id]': getDevId(), + 'from_firework': 0, + 'build': 0, + 'mobi_app': 'web', + 'csrf_token': csrf, + 'csrf': csrf, + }, + options: Options( + contentType: Headers.formUrlEncodedContentType, + ), + ); if (res.data['code'] == 0) { return { 'status': true, 'data': res.data['data'], }; } else { - return { - 'status': false, - 'date': [], - 'msg': "message: ${res.data['message']}," - " msg: ${res.data['msg']}," - " code: ${res.data['code']}", - }; + return {'status': false, 'date': [], 'msg': res.data['message']}; } } @@ -220,4 +204,87 @@ class MsgHttp { } 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']}; + } + } } diff --git a/lib/models/common/subtitle_type.dart b/lib/models/common/subtitle_type.dart index 54b52e8e..ac3ee3e0 100644 --- a/lib/models/common/subtitle_type.dart +++ b/lib/models/common/subtitle_type.dart @@ -9,6 +9,14 @@ enum SubtitleType { zhHans, // 英文(美国) enUS, + // 中文繁体 + zhTW, + // + en, + // + pt, + // + es, } extension SubtitleTypeExtension on SubtitleType { @@ -24,6 +32,14 @@ extension SubtitleTypeExtension on SubtitleType { return '中文(简体)'; case SubtitleType.enUS: return '英文(美国)'; + case SubtitleType.zhTW: + return '中文(繁体)'; + case SubtitleType.en: + return '英文'; + case SubtitleType.pt: + return '葡萄牙语'; + case SubtitleType.es: + return '西班牙语'; } } } @@ -41,6 +57,14 @@ extension SubtitleIdExtension on SubtitleType { return 'zh-Hans'; case SubtitleType.enUS: return 'en-US'; + case SubtitleType.zhTW: + return 'zh-TW'; + case SubtitleType.en: + return 'en'; + case SubtitleType.pt: + return 'pt'; + case SubtitleType.es: + return 'es'; } } } @@ -58,6 +82,14 @@ extension SubtitleCodeExtension on SubtitleType { return 4; case SubtitleType.enUS: return 5; + case SubtitleType.zhTW: + return 6; + case SubtitleType.en: + return 7; + case SubtitleType.pt: + return 8; + case SubtitleType.es: + return 9; } } } diff --git a/lib/models/msg/like.dart b/lib/models/msg/like.dart new file mode 100644 index 00000000..b279131b --- /dev/null +++ b/lib/models/msg/like.dart @@ -0,0 +1,183 @@ +class MessageLikeModel { + MessageLikeModel({ + this.latest, + this.total, + }); + + Latest? latest; + Total? total; + + factory MessageLikeModel.fromJson(Map 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 json) => Latest( + items: json["items"], + lastViewAt: json["last_view_at"], + ); +} + +class Total { + Total({ + this.cursor, + this.items, + }); + + Cursor? cursor; + List? items; + + factory Total.fromJson(Map json) => Total( + cursor: Cursor.fromJson(json['cursor']), + items: json["items"] == null + ? [] + : json["items"].map((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 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? users; + MessageLikeItemItem? item; + int? counts; + int? likeTime; + int? noticeState; + bool isExpand; + + factory MessageLikeItem.fromJson(Map json) => + MessageLikeItem( + id: json["id"], + users: json["users"] == null + ? [] + : json["users"].map((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 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 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"], + ); +} diff --git a/lib/models/msg/reply.dart b/lib/models/msg/reply.dart new file mode 100644 index 00000000..be617088 --- /dev/null +++ b/lib/models/msg/reply.dart @@ -0,0 +1,168 @@ +class MessageReplyModel { + MessageReplyModel({ + this.cursor, + this.items, + }); + + Cursor? cursor; + List? items; + + MessageReplyModel.fromJson(Map json) { + cursor = Cursor.fromJson(json['cursor']); + items = json["items"] != null + ? json["items"].map((e) { + return MessageReplyItem.fromJson(e); + }).toList() + : []; + } +} + +class Cursor { + Cursor({ + this.id, + this.isEnd, + this.time, + }); + + int? id; + bool? isEnd; + int? time; + + Cursor.fromJson(Map 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 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 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 json) { + mid = json['mid']; + fans = json['fans']; + nickname = json['nickname']; + avatar = json['avatar']; + midLink = json['mid_link']; + follow = json['follow']; + } +} diff --git a/lib/pages/bangumi/introduction/controller.dart b/lib/pages/bangumi/introduction/controller.dart index 65cd5dd8..208a85e4 100644 --- a/lib/pages/bangumi/introduction/controller.dart +++ b/lib/pages/bangumi/introduction/controller.dart @@ -225,6 +225,8 @@ class BangumiIntroController extends GetxController { videoDetailCtr.oid.value = aid; videoDetailCtr.cover.value = cover; videoDetailCtr.queryVideoUrl(); + videoDetailCtr.getSubtitle(); + videoDetailCtr.setSubtitleContent(); // 重新请求评论 try { /// 未渲染回复组件时可能异常 diff --git a/lib/pages/member/widgets/profile.dart b/lib/pages/member/widgets/profile.dart index a708a35e..8c6385db 100644 --- a/lib/pages/member/widgets/profile.dart +++ b/lib/pages/member/widgets/profile.dart @@ -208,7 +208,17 @@ class ProfilePanel extends StatelessWidget { const SizedBox(width: 8), Expanded( child: TextButton( - onPressed: () {}, + onPressed: () { + Get.toNamed( + '/whisperDetail', + parameters: { + 'name': memberInfo.name!, + 'face': memberInfo.face!, + 'mid': memberInfo.mid.toString(), + 'heroTag': ctr.heroTag!, + }, + ); + }, style: TextButton.styleFrom( backgroundColor: Theme.of(context) .colorScheme diff --git a/lib/pages/message/at/controller.dart b/lib/pages/message/at/controller.dart new file mode 100644 index 00000000..af08987f --- /dev/null +++ b/lib/pages/message/at/controller.dart @@ -0,0 +1,3 @@ +import 'package:get/get.dart'; + +class MessageAtController extends GetxController {} diff --git a/lib/pages/message/at/index.dart b/lib/pages/message/at/index.dart new file mode 100644 index 00000000..b2c573e6 --- /dev/null +++ b/lib/pages/message/at/index.dart @@ -0,0 +1,4 @@ +library message_at; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/message/at/view.dart b/lib/pages/message/at/view.dart new file mode 100644 index 00000000..9c48ec99 --- /dev/null +++ b/lib/pages/message/at/view.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class MessageAtPage extends StatefulWidget { + const MessageAtPage({super.key}); + + @override + State createState() => _MessageAtPageState(); +} + +class _MessageAtPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('@我的'), + ), + ); + } +} diff --git a/lib/pages/message/like/controller.dart b/lib/pages/message/like/controller.dart new file mode 100644 index 00000000..9b09f89a --- /dev/null +++ b/lib/pages/message/like/controller.dart @@ -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 likeItems = [].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(); + } +} diff --git a/lib/pages/message/like/index.dart b/lib/pages/message/like/index.dart new file mode 100644 index 00000000..4f9c143a --- /dev/null +++ b/lib/pages/message/like/index.dart @@ -0,0 +1,4 @@ +library message_like; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/message/like/view.dart b/lib/pages/message/like/view.dart new file mode 100644 index 00000000..e677fb25 --- /dev/null +++ b/lib/pages/message/like/view.dart @@ -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 createState() => _MessageLikePageState(); +} + +class _MessageLikePageState extends State { + 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( + '/video?bvid=$bvid&cid=$cid', + arguments: { + '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), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/message/reply/controller.dart b/lib/pages/message/reply/controller.dart new file mode 100644 index 00000000..d62662df --- /dev/null +++ b/lib/pages/message/reply/controller.dart @@ -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 replyItems = [].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; + } +} diff --git a/lib/pages/message/reply/index.dart b/lib/pages/message/reply/index.dart new file mode 100644 index 00000000..969d03dd --- /dev/null +++ b/lib/pages/message/reply/index.dart @@ -0,0 +1,4 @@ +library message_reply; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/message/reply/view.dart b/lib/pages/message/reply/view.dart new file mode 100644 index 00000000..881f8650 --- /dev/null +++ b/lib/pages/message/reply/view.dart @@ -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 createState() => _MessageReplyPageState(); +} + +class _MessageReplyPageState extends State { + 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( + '/video?bvid=$bvid&cid=$cid', + arguments: { + '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 spanChilds = []; + if (atDetails!.isNotEmpty) { + final String patternStr = + atDetails.map((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); +} diff --git a/lib/pages/message/system/controller.dart b/lib/pages/message/system/controller.dart new file mode 100644 index 00000000..ad28af56 --- /dev/null +++ b/lib/pages/message/system/controller.dart @@ -0,0 +1,3 @@ +import 'package:get/get.dart'; + +class MessageSystemController extends GetxController {} diff --git a/lib/pages/message/system/index.dart b/lib/pages/message/system/index.dart new file mode 100644 index 00000000..70b9fc6d --- /dev/null +++ b/lib/pages/message/system/index.dart @@ -0,0 +1,4 @@ +library message_system; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/message/system/view.dart b/lib/pages/message/system/view.dart new file mode 100644 index 00000000..da0f1219 --- /dev/null +++ b/lib/pages/message/system/view.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class MessageSystemPage extends StatefulWidget { + const MessageSystemPage({super.key}); + + @override + State createState() => _MessageSystemPageState(); +} + +class _MessageSystemPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('系统通知'), + ), + ); + } +} diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 9c542f21..1a1b1b74 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -436,6 +436,7 @@ class VideoIntroController extends GetxController { videoDetailCtr.cover.value = cover; videoDetailCtr.queryVideoUrl(); videoDetailCtr.getSubtitle(); + videoDetailCtr.setSubtitleContent(); // 重新请求评论 try { /// 未渲染回复组件时可能异常 diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index 08e4d405..02d004e8 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -645,7 +645,7 @@ InlineSpan buildContent( '', ); } else { - Uri uri = Uri.parse(matchStr); + Uri uri = Uri.parse(matchStr.replaceAll('/?', '?')); SchemeEntity scheme = SchemeEntity( scheme: uri.scheme, host: uri.host, diff --git a/lib/pages/video/detail/reply_new/view.dart b/lib/pages/video/detail/reply_new/view.dart index 40267559..0f04455f 100644 --- a/lib/pages/video/detail/reply_new/view.dart +++ b/lib/pages/video/detail/reply_new/view.dart @@ -117,6 +117,7 @@ class _VideoReplyNewDialogState extends State final String newText = currentText.substring(0, cursorPosition) + emote.text! + currentText.substring(cursorPosition); + message.value = newText; _replyContentController.value = TextEditingValue( text: newText, selection: diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index ea29bf78..02a0bbe1 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -515,6 +515,13 @@ class _VideoDetailPageState extends State showEposideCb: () => vdCtr.videoType == SearchType.video ? videoIntroController.showEposideHandler() : bangumiIntroController.showEposideHandler(), + fullScreenCb: (bool status) { + if (status) { + videoHeight.value = Get.size.height; + } else { + videoHeight.value = defaultVideoHeight; + } + }, ), ); }, diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart index f51edff1..5072377a 100644 --- a/lib/pages/video/detail/widgets/header_control.dart +++ b/lib/pages/video/detail/widgets/header_control.dart @@ -433,42 +433,47 @@ class _HeaderControlState extends State { return AlertDialog( title: const Text('选择字幕'), contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 18), - content: StatefulBuilder(builder: (context, StateSetter setState) { - return len == 0 - ? const SizedBox( - height: 60, - child: Center( - child: Text('没有字幕'), - ), - ) - : Column( - mainAxisSize: MainAxisSize.min, - children: [ - RadioListTile( - value: -1, - title: const Text('关闭字幕'), - groupValue: tempThemeValue, - onChanged: (value) { - tempThemeValue = value!; - widget.controller?.toggleSubtitle(value); - Get.back(); - }, + content: StatefulBuilder( + builder: (context, StateSetter setState) { + return len == 0 + ? const SizedBox( + height: 60, + child: Center( + child: Text('没有字幕'), ), - ...widget.videoDetailCtr!.subtitles - .map((e) => RadioListTile( - value: e.code, - title: Text(e.title), - groupValue: tempThemeValue, - onChanged: (value) { - tempThemeValue = value!; - widget.controller?.toggleSubtitle(value); - Get.back(); - }, - )) - .toList(), - ], - ); - }), + ) + : SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RadioListTile( + value: -1, + title: const Text('关闭字幕'), + groupValue: tempThemeValue, + onChanged: (value) { + tempThemeValue = value!; + widget.controller?.toggleSubtitle(value); + Get.back(); + }, + ), + ...widget.videoDetailCtr!.subtitles + .map((e) => RadioListTile( + value: e.code, + title: Text(e.title), + groupValue: tempThemeValue, + onChanged: (value) { + tempThemeValue = value!; + widget.controller + ?.toggleSubtitle(value); + Get.back(); + }, + )) + .toList(), + ], + ), + ); + }, + ), ); }); } diff --git a/lib/pages/whisper/controller.dart b/lib/pages/whisper/controller.dart index c82cab5b..2614bf5a 100644 --- a/lib/pages/whisper/controller.dart +++ b/lib/pages/whisper/controller.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/http/msg.dart'; import 'package:pilipala/models/msg/account.dart'; @@ -7,6 +8,38 @@ class WhisperController extends GetxController { RxList sessionList = [].obs; RxList accountList = [].obs; 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 { if (isLoading) return; @@ -62,4 +95,31 @@ class WhisperController extends GetxController { Future onRefresh() async { 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(); + } + } } diff --git a/lib/pages/whisper/view.dart b/lib/pages/whisper/view.dart index fa95463b..e31e942e 100644 --- a/lib/pages/whisper/view.dart +++ b/lib/pages/whisper/view.dart @@ -1,6 +1,7 @@ import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.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/utils/utils.dart'; @@ -44,147 +45,152 @@ class _WhisperPageState extends State { appBar: AppBar( title: const Text('消息'), ), - body: Column( - children: [ - // LayoutBuilder( - // builder: (BuildContext context, BoxConstraints constraints) { - // // 在这里根据父级容器的约束条件构建小部件树 - // return Padding( - // padding: const EdgeInsets.only(left: 20, right: 20), - // child: SizedBox( - // height: constraints.maxWidth / 5, - // child: GridView.count( - // primary: false, - // crossAxisCount: 4, - // padding: const EdgeInsets.all(0), - // childAspectRatio: 1.25, - // children: [ - // Column( - // crossAxisAlignment: CrossAxisAlignment.center, - // mainAxisAlignment: MainAxisAlignment.center, - // children: [ - // SizedBox( - // width: 36, - // height: 36, - // child: IconButton( - // style: ButtonStyle( - // padding: - // MaterialStateProperty.all(EdgeInsets.zero), - // backgroundColor: - // MaterialStateProperty.resolveWith((states) { - // return Theme.of(context) - // .colorScheme - // .primary - // .withOpacity(0.1); - // }), - // ), - // onPressed: () {}, - // icon: Icon( - // Icons.message_outlined, - // size: 18, - // color: Theme.of(context).colorScheme.primary, - // ), - // ), - // ), - // const SizedBox(height: 6), - // const Text('回复我的', style: TextStyle(fontSize: 13)) - // ], - // ), - // ], - // ), - // ), - // ); - // }, - // ), - Expanded( - child: RefreshIndicator( - onRefresh: () async { - await _whisperController.onRefresh(); - }, - child: SingleChildScrollView( - controller: _scrollController, - child: 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), - ); - }, + body: RefreshIndicator( + onRefresh: () async { + _whisperController.unread(); + await _whisperController.onRefresh(); + }, + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + children: [ + LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + // 在这里根据父级容器的约束条件构建小部件树 + return Padding( + padding: const EdgeInsets.only(left: 20, right: 20), + child: SizedBox( + height: constraints.maxWidth / 4, + child: Obx( + () => GridView.count( + primary: false, + crossAxisCount: 4, + padding: const EdgeInsets.all(0), + children: [ + ..._whisperController.noticesList.map((element) { + return InkWell( + onTap: () { + Get.toNamed(element['path']); + + if (element['count'] > 0) { + element['count'] = 0; + } + _whisperController.noticesList.refresh(); + }, + onLongPress: () {}, + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Badge( + isLabelVisible: element['count'] > 0, + label: Text(element['count'] > 99 + ? '99+' + : element['count'].toString()), + child: Padding( + padding: const EdgeInsets.all(10), + child: Icon( + element['icon'], + size: 21, + color: Theme.of(context) + .colorScheme + .primary, + ), + ), + ), + const SizedBox(height: 4), + Text(element['title']) + ], ), - ); - } else { - // 请求错误 - return Center( - child: Text(data?['msg'] ?? '请求异常'), - ); - } + ); + }).toList(), + ], + ), + ), + ), + ); + }, + ), + 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 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, - ), - ), - ); - }, + // 请求错误 + 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, + ), + ), + ); + }, + ); + } + }, ), - ), + ], ), - ], + ), ), ); } @@ -202,7 +208,10 @@ class SessionItem extends StatelessWidget { @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; @@ -214,6 +223,7 @@ class SessionItem extends StatelessWidget { 'name': sessionItem.accountInfo.name, 'face': sessionItem.accountInfo.face, 'mid': sessionItem.accountInfo.mid.toString(), + 'heroTag': heroTag, }, ); }, @@ -221,22 +231,27 @@ class SessionItem extends StatelessWidget { isLabelVisible: sessionItem.unreadCount > 0, label: Text(sessionItem.unreadCount.toString()), alignment: Alignment.topRight, - child: NetworkImgLayer( - width: 45, - height: 45, - type: 'avatar', - src: sessionItem.accountInfo.face, + child: Hero( + tag: heroTag, + child: NetworkImgLayer( + width: 45, + height: 45, + type: 'avatar', + src: sessionItem.accountInfo.face, + ), ), ), title: Text(sessionItem.accountInfo.name), subtitle: Text( - content != null && content != '' - ? (content['text'] ?? - content['content'] ?? - content['title'] ?? - content['reply_content'] ?? - '不支持的消息类型') - : '不支持的消息类型', + msgStatus == 1 + ? '你撤回了一条消息' + : content != null && content != '' + ? (content['text'] ?? + content['content'] ?? + content['title'] ?? + content['reply_content'] ?? + '不支持的消息类型') + : '不支持的消息类型', maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context) @@ -245,10 +260,10 @@ class SessionItem extends StatelessWidget { .copyWith(color: Theme.of(context).colorScheme.outline)), trailing: Text( Utils.dateFormat(sessionItem.lastMsg.timestamp), - style: Theme.of(context) - .textTheme - .labelSmall! - .copyWith(color: Theme.of(context).colorScheme.outline), + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelSmall!.fontSize, + color: Theme.of(context).colorScheme.outline, + ), ), ); } diff --git a/lib/pages/whisper_detail/controller.dart b/lib/pages/whisper_detail/controller.dart index 6e950f81..32e0ceb0 100644 --- a/lib/pages/whisper_detail/controller.dart +++ b/lib/pages/whisper_detail/controller.dart @@ -1,30 +1,40 @@ +import 'dart:async'; +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/http/msg.dart'; import 'package:pilipala/models/msg/session.dart'; +import 'package:pilipala/pages/whisper/index.dart'; import '../../utils/feed_back.dart'; import '../../utils/storage.dart'; class WhisperDetailController extends GetxController { - late int talkerId; + int? talkerId; late String name; late String face; late String mid; + late String heroTag; RxList messageList = [].obs; //表情转换图片规则 - List? eInfos; + RxList eInfos = [].obs; final TextEditingController replyContentController = TextEditingController(); Box userInfoCache = GStrorage.userInfo; + List emoteList = []; @override void 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']!; face = Get.parameters['face']!; mid = Get.parameters['mid']!; + heroTag = Get.parameters['heroTag']!; } Future querySessionMsg() async { @@ -34,7 +44,7 @@ class WhisperDetailController extends GetxController { if (messageList.isNotEmpty) { ackSessionMsg(); if (res['data'].eInfos != null) { - eInfos = res['data'].eInfos; + eInfos.value = res['data'].eInfos; } } } else { @@ -73,9 +83,64 @@ class WhisperDetailController extends GetxController { msgType: 1, ); 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.refreshLastMsg(talkerId!, message); + } catch (_) {} } else { 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.removeSessionMsg(talkerId!); + Get.back(); + } catch (_) {} + } + }, + child: const Text('确认'), + ), + ], + ); + }, + ); + } } diff --git a/lib/pages/whisper_detail/view.dart b/lib/pages/whisper_detail/view.dart index 1701be33..912b5dc5 100644 --- a/lib/pages/whisper_detail/view.dart +++ b/lib/pages/whisper_detail/view.dart @@ -1,9 +1,12 @@ import 'dart:async'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:hive/hive.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/video/detail/reply_new/toolbar_icon_button.dart'; import 'package:pilipala/pages/whisper_detail/controller.dart'; import 'package:pilipala/utils/feed_back.dart'; import '../../utils/storage.dart'; @@ -24,9 +27,9 @@ class _WhisperDetailPageState extends State late TextEditingController _replyContentController; final FocusNode replyContentFocusNode = FocusNode(); final _debouncer = Debouncer(milliseconds: 200); // 设置延迟时间 - late double emoteHeight = 0.0; + late double emoteHeight = 230.0; double keyboardHeight = 0.0; // 键盘高度 - String toolbarType = 'input'; + RxString toolbarType = ''.obs; Box userInfoCache = GStrorage.userInfo; @override @@ -41,9 +44,7 @@ class _WhisperDetailPageState extends State _focuslistener() { replyContentFocusNode.addListener(() { if (replyContentFocusNode.hasFocus) { - setState(() { - toolbarType = 'input'; - }); + toolbarType.value = 'input'; } }); } @@ -52,7 +53,7 @@ class _WhisperDetailPageState extends State void didChangeMetrics() { super.didChangeMetrics(); final String routePath = Get.currentRoute; - if (mounted && routePath.startsWith('/whisper_detail')) { + if (mounted && routePath.startsWith('/whisperDetail')) { WidgetsBinding.instance.addPostFrameCallback((_) { // 键盘高度 final viewInsets = EdgeInsets.fromViewPadding( @@ -61,8 +62,11 @@ class _WhisperDetailPageState extends State if (mounted) { if (keyboardHeight == 0) { setState(() { - emoteHeight = keyboardHeight = + keyboardHeight = keyboardHeight == 0.0 ? viewInsets.bottom : keyboardHeight; + if (keyboardHeight != 0) { + emoteHeight = keyboardHeight; + } }); } } @@ -79,6 +83,23 @@ class _WhisperDetailPageState extends State 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 Widget build(BuildContext context) { return Scaffold( @@ -88,30 +109,20 @@ class _WhisperDetailPageState extends State width: double.infinity, height: 50, child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SizedBox( width: 34, height: 34, child: IconButton( - style: ButtonStyle( - padding: MaterialStateProperty.all(EdgeInsets.zero), - backgroundColor: MaterialStateProperty.resolveWith( - (Set states) { - return Theme.of(context) - .colorScheme - .primaryContainer - .withOpacity(0.6); - }), - ), onPressed: () => Get.back(), icon: Icon( - Icons.arrow_back_outlined, + Icons.arrow_back_ios, size: 18, color: Theme.of(context).colorScheme.onPrimaryContainer, ), ), ), + const SizedBox(width: 10), GestureDetector( onTap: () { feedBack(); @@ -125,13 +136,16 @@ class _WhisperDetailPageState extends State }, child: Row( children: [ - NetworkImgLayer( - width: 34, - height: 34, - type: 'avatar', - src: _whisperDetailController.face, + Hero( + tag: _whisperDetailController.heroTag, + child: NetworkImgLayer( + width: 34, + height: 34, + type: 'avatar', + src: _whisperDetailController.face, + ), ), - const SizedBox(width: 6), + const SizedBox(width: 10), Text( _whisperDetailController.name, style: Theme.of(context).textTheme.titleMedium, @@ -143,155 +157,171 @@ class _WhisperDetailPageState extends State ], ), ), + actions: [ + PopupMenuButton( + icon: const Icon(Icons.more_vert_outlined, size: 20), + itemBuilder: (BuildContext context) => [ + PopupMenuItem( + onTap: () => _whisperDetailController.removeSession(context), + child: const Text('关闭会话'), + ) + ], + ), + const SizedBox(width: 14) + ], ), - body: GestureDetector( - onTap: () { - FocusScope.of(context).unfocus(); - setState(() { - keyboardHeight = 0; - }); - }, - child: FutureBuilder( - future: _futureBuilderFuture, - builder: (BuildContext context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - if (snapshot.data == null) { - return const SizedBox(); - } - final Map data = snapshot.data as Map; - if (data['status']) { - List messageList = _whisperDetailController.messageList; - return Obx( - () => messageList.isEmpty - ? const SizedBox() - : ListView.builder( - itemCount: messageList.length, - shrinkWrap: true, - reverse: true, - itemBuilder: (_, int i) { - if (i == 0) { - return Column( - children: [ - ChatItem( - item: messageList[i], - e_infos: _whisperDetailController.eInfos), - const SizedBox(height: 12), - ], - ); - } else { - return ChatItem( - item: messageList[i], - e_infos: _whisperDetailController.eInfos); - } - }, - ), - ); - } else { - // 请求错误 - return const SizedBox(); - } - } else { - // 骨架屏 - return const SizedBox(); - } - }, - ), - ), - // resizeToAvoidBottomInset: true, - bottomNavigationBar: Container( - 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), + body: Column( + children: [ + Expanded( + child: GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); + toolbarType.value = ''; + }, + child: FutureBuilder( + future: _futureBuilderFuture, + builder: (BuildContext context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.data == null) { + return const SizedBox(); + } + final Map data = snapshot.data as Map; + if (data['status']) { + List messageList = _whisperDetailController.messageList; + return Obx( + () => messageList.isEmpty + ? const SizedBox() + : Align( + alignment: Alignment.topCenter, + child: ListView.builder( + itemCount: messageList.length, + shrinkWrap: true, + reverse: true, + itemBuilder: (_, int i) { + if (i == 0) { + return Column( + children: [ + ChatItem( + item: messageList[i], + e_infos: _whisperDetailController + .eInfos), + const SizedBox(height: 20), + ], + ); + } else { + return ChatItem( + item: messageList[i], + e_infos: + _whisperDetailController.eInfos); + } + }, + ), + ), + ); + } else { + // 请求错误 + return const SizedBox(); + } + } else { + // 骨架屏 + return const SizedBox(); + } + }, + ), ), ), - ), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // IconButton( - // onPressed: () {}, - // icon: Icon( - // Icons.add_circle_outline, - // color: Theme.of(context).colorScheme.outline, - // ), - // ), - IconButton( - onPressed: () { - // if (toolbarType == 'input') { - // setState(() { - // toolbarType = 'emote'; - // }); - // } - // FocusScope.of(context).unfocus(); - }, - icon: Icon( - Icons.emoji_emotions_outlined, - color: Theme.of(context).colorScheme.outline, + Obx( + () => Container( + padding: EdgeInsets.fromLTRB( + 8, + 12, + 12, + toolbarType.value == '' + ? MediaQuery.of(context).padding.bottom + 6 + : 6, + ), + decoration: BoxDecoration( + border: Border( + top: BorderSide( + width: 1, + color: Colors.grey.withOpacity(0.15), ), ), - Expanded( - child: Container( - height: 45, - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .primary - .withOpacity(0.08), - borderRadius: BorderRadius.circular(40.0), - ), - child: TextField( - readOnly: true, - 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), // 内边距 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ToolbarIconButton( + onPressed: () { + if (toolbarType.value == '') { + toolbarType.value = 'emote'; + } else if (toolbarType.value == 'input') { + FocusScope.of(context).unfocus(); + toolbarType.value = 'emote'; + } else if (toolbarType.value == 'emote') { + FocusScope.of(context).requestFocus(); + } + }, + icon: const Icon(Icons.emoji_emotions_outlined, size: 22), + toolbarType: toolbarType.value, + selected: false, + ), + const SizedBox(width: 4), + 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( - // onPressed: _whisperDetailController.sendMsg, - onPressed: null, - icon: Icon( - Icons.send, - color: Theme.of(context).colorScheme.outline, + IconButton( + onPressed: _whisperDetailController.sendMsg, + icon: Icon( + Icons.send, + color: Theme.of(context).colorScheme.outline, + ), ), - ), - // const SizedBox(width: 16), - ], + ], + ), ), - AnimatedSize( - curve: Curves.easeInOut, - duration: const Duration(milliseconds: 300), + ), + Obx( + () => AnimatedSize( + curve: Curves.linear, + duration: const Duration(milliseconds: 200), child: SizedBox( width: double.infinity, - height: toolbarType == 'input' ? keyboardHeight : emoteHeight, + height: toolbarType.value == 'input' + ? keyboardHeight + : toolbarType.value == 'emote' + ? emoteHeight + : 0, child: EmotePanel( - onChoose: (package, emote) => {}, + onChoose: (package, emote) => onChooseEmote(package, emote), ), ), ), - ], - ), + ), + ], ), + resizeToAvoidBottomInset: false, ); } } diff --git a/lib/pages/whisper_detail/widget/chat_item.dart b/lib/pages/whisper_detail/widget/chat_item.dart index 0d37e8b3..77e38073 100644 --- a/lib/pages/whisper_detail/widget/chat_item.dart +++ b/lib/pages/whisper_detail/widget/chat_item.dart @@ -1,7 +1,7 @@ // ignore_for_file: must_be_immutable +// ignore_for_file: constant_identifier_names import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -9,7 +9,6 @@ import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/utils/route_push.dart'; import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/storage.dart'; - import '../../../http/search.dart'; enum MsgType { @@ -69,9 +68,13 @@ class ChatItem extends StatelessWidget { Color textColor(BuildContext context) { return isOwner ? 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) { var text = content['content']; if (e_infos != null) { @@ -386,73 +389,97 @@ class ChatItem extends StatelessWidget { ? messageContent(context) : isRevoke ? const SizedBox() - : Row( - children: [ - if (!isOwner) const SizedBox(width: 12), - if (isOwner) const Spacer(), - Container( - constraints: const BoxConstraints( - maxWidth: 300.0, // 设置最大宽度为200.0 - ), - decoration: BoxDecoration( - color: isOwner - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.secondaryContainer, - borderRadius: BorderRadius.only( - topLeft: const Radius.circular(16), - topRight: const Radius.circular(16), - bottomLeft: Radius.circular(isOwner ? 16 : 6), - bottomRight: Radius.circular(isOwner ? 6 : 16), + : Container( + padding: const EdgeInsets.only(top: 6, bottom: 6), + decoration: BoxDecoration( + border: Border( + left: item.msgStatus == 1 && !isOwner + ? BorderSide( + width: 4, color: Theme.of(context).dividerColor) + : BorderSide.none, + right: item.msgStatus == 1 && isOwner + ? BorderSide( + width: 4, color: Theme.of(context).primaryColor) + : BorderSide.none, + )), + child: Row( + mainAxisAlignment: !isOwner + ? MainAxisAlignment.start + : MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(width: safeDistanceval), + 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: Column( + crossAxisAlignment: isOwner + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + messageContent(context), + SizedBox(height: isPic ? 7 : 4), + 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), - padding: EdgeInsets.only( - top: 8, - bottom: 6, - left: isPic ? 8 : 12, - right: isPic ? 8 : 12, - ), - 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), - ], + const SizedBox(width: safeDistanceval), + ], + ), ); } } diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index 34140be8..f3e0946b 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -39,6 +39,7 @@ class PLVideoPlayer extends StatefulWidget { this.customWidget, this.customWidgets, this.showEposideCb, + this.fullScreenCb, super.key, }); @@ -52,6 +53,7 @@ class PLVideoPlayer extends StatefulWidget { final Widget? customWidget; final List? customWidgets; final Function? showEposideCb; + final Function? fullScreenCb; @override State createState() => _PLVideoPlayerState(); @@ -335,7 +337,10 @@ class _PLVideoPlayerState extends State color: Colors.white, ), ), - fuc: () => _.triggerFullScreen(status: !_.isFullScreen.value), + fuc: () { + _.triggerFullScreen(status: !_.isFullScreen.value); + widget.fullScreenCb?.call(!_.isFullScreen.value); + }, ), }; final List list = []; @@ -940,7 +945,7 @@ class _PLVideoPlayerState extends State begin: 0.0, end: _hideSeekBackwardButton.value ? 0.0 : 1.0, ), - duration: const Duration(milliseconds: 200), + duration: const Duration(milliseconds: 150), builder: (BuildContext context, double value, Widget? child) => Opacity( @@ -983,7 +988,7 @@ class _PLVideoPlayerState extends State begin: 0.0, end: _hideSeekForwardButton.value ? 0.0 : 1.0, ), - duration: const Duration(milliseconds: 200), + duration: const Duration(milliseconds: 150), builder: (BuildContext context, double value, Widget? child) => Opacity( diff --git a/lib/plugin/pl_player/widgets/backward_seek.dart b/lib/plugin/pl_player/widgets/backward_seek.dart index 8fddf80a..8289d77c 100644 --- a/lib/plugin/pl_player/widgets/backward_seek.dart +++ b/lib/plugin/pl_player/widgets/backward_seek.dart @@ -30,14 +30,14 @@ class BackwardSeekIndicatorState extends State { @override void initState() { super.initState(); - timer = Timer(const Duration(milliseconds: 400), () { + timer = Timer(const Duration(milliseconds: 200), () { widget.onSubmitted.call(value); }); } void increment() { timer?.cancel(); - timer = Timer(const Duration(milliseconds: 400), () { + timer = Timer(const Duration(milliseconds: 200), () { widget.onSubmitted.call(value); }); widget.onChanged.call(value); diff --git a/lib/plugin/pl_player/widgets/forward_seek.dart b/lib/plugin/pl_player/widgets/forward_seek.dart index 7e3886ce..3f68fe0d 100644 --- a/lib/plugin/pl_player/widgets/forward_seek.dart +++ b/lib/plugin/pl_player/widgets/forward_seek.dart @@ -30,14 +30,14 @@ class ForwardSeekIndicatorState extends State { @override void initState() { super.initState(); - timer = Timer(const Duration(milliseconds: 400), () { + timer = Timer(const Duration(milliseconds: 200), () { widget.onSubmitted.call(value); }); } void increment() { timer?.cancel(); - timer = Timer(const Duration(milliseconds: 400), () { + timer = Timer(const Duration(milliseconds: 200), () { widget.onSubmitted.call(value); }); widget.onChanged.call(value); diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 2ca333f8..a6b48f0d 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -4,6 +4,10 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:hive/hive.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 '../pages/about/index.dart'; @@ -178,6 +182,15 @@ class Routes { // 操作菜单 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()), ]; } diff --git a/lib/utils/app_scheme.dart b/lib/utils/app_scheme.dart index a83b7809..17d20bcd 100644 --- a/lib/utils/app_scheme.dart +++ b/lib/utils/app_scheme.dart @@ -1,5 +1,6 @@ import 'package:appscheme/appscheme.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:pilipala/utils/route_push.dart'; @@ -38,60 +39,82 @@ class PiliSchame { final String host = value.host; final String path = value.path; if (scheme == 'bilibili') { - if (host == 'root') { - Navigator.popUntil( - Get.context!, (Route route) => route.isFirst); - } else if (host == 'space') { - final String mid = path.split('/').last; - Get.toNamed( - '/member?mid=$mid', - arguments: {'face': null}, - ); - } else if (host == 'video') { - String pathQuery = path.split('/').last; - final numericRegex = RegExp(r'^[0-9]+$'); - if (numericRegex.hasMatch(pathQuery)) { - pathQuery = 'AV$pathQuery'; - } - Map map = IdUtils.matchAvorBv(input: pathQuery); - if (map.containsKey('AV')) { - _videoPush(map['AV'], null); - } else if (map.containsKey('BV')) { - _videoPush(null, map['BV']); - } else { - SmartDialog.showToast('投稿匹配失败'); - } - } else if (host == 'live') { - final String roomId = path.split('/').last; - Get.toNamed('/liveRoom?roomid=$roomId', - arguments: {'liveItem': null, 'heroTag': roomId}); - } else if (host == 'bangumi') { - if (path.startsWith('/season')) { - final String seasonId = path.split('/').last; - RoutePush.bangumiPush(int.parse(seasonId), null); - } - } else if (host == 'opus') { - if (path.startsWith('/detail')) { - var opusId = path.split('/').last; - Get.toNamed( - '/webview', - parameters: { - 'url': 'https://www.bilibili.com/opus/$opusId', - 'type': 'url', - 'pageTitle': '', - }, + switch (host) { + case 'root': + Navigator.popUntil( + Get.context!, (Route route) => route.isFirst); + break; + case 'space': + final String mid = path.split('/').last; + Get.toNamed( + '/member?mid=$mid', + arguments: {'face': null}, ); - } - } else if (host == 'search') { - Get.toNamed('/searchResult', parameters: {'keyword': ''}); - } else if (host == 'article') { - final String id = path.split('/').last.split('?').first; - Get.toNamed('/htmlRender', parameters: { - 'url': 'https://www.bilibili.com/read/cv$id', - 'title': 'cv$id', - 'id': 'cv$id', - 'dynamicType': 'read' - }); + break; + case 'video': + String pathQuery = path.split('/').last; + final numericRegex = RegExp(r'^[0-9]+$'); + if (numericRegex.hasMatch(pathQuery)) { + pathQuery = 'AV$pathQuery'; + } + Map map = IdUtils.matchAvorBv(input: pathQuery); + if (map.containsKey('AV')) { + _videoPush(map['AV'], null); + } else if (map.containsKey('BV')) { + _videoPush(null, map['BV']); + } else { + SmartDialog.showToast('投稿匹配失败'); + } + break; + case 'live': + final String roomId = path.split('/').last; + Get.toNamed( + '/liveRoom?roomid=$roomId', + arguments: {'liveItem': null, 'heroTag': roomId}, + ); + break; + case 'bangumi': + if (path.startsWith('/season')) { + final String seasonId = path.split('/').last; + RoutePush.bangumiPush(int.parse(seasonId), null); + } + break; + case 'opus': + if (path.startsWith('/detail')) { + var opusId = path.split('/').last; + Get.toNamed( + '/webview', + parameters: { + 'url': 'https://www.bilibili.com/opus/$opusId', + 'type': 'url', + 'pageTitle': '', + }, + ); + } + break; + case 'search': + Get.toNamed('/searchResult', parameters: {'keyword': ''}); + break; + case 'article': + final String id = path.split('/').last.split('?').first; + Get.toNamed('/htmlRender', parameters: { + 'url': 'https://www.bilibili.com/read/cv$id', + 'title': 'cv$id', + 'id': 'cv$id', + 'dynamicType': 'read' + }); + break; + case 'pgc': + if (path.contains('ep')) { + final String lastPathSegment = path.split('/').last; + RoutePush.bangumiPush( + null, int.parse(lastPathSegment.split('?').first)); + } + break; + default: + SmartDialog.showToast('未匹配地址,请联系开发者'); + Clipboard.setData(ClipboardData(text: value.toJson().toString())); + break; } } if (scheme == 'https') { @@ -196,9 +219,7 @@ class PiliSchame { parameters: {'url': redirectUrl, 'type': 'url', 'pageTitle': ''}, ); } - } - - if (path != null) { + } else if (path != null) { final String area = path.split('/').last; switch (area) { case 'bangumi':