diff --git a/assets/images/lv/lv0.png b/assets/images/lv/lv0.png new file mode 100644 index 000000000..3b9999cf7 Binary files /dev/null and b/assets/images/lv/lv0.png differ diff --git a/assets/images/lv/lv1.png b/assets/images/lv/lv1.png new file mode 100644 index 000000000..9973e4e7e Binary files /dev/null and b/assets/images/lv/lv1.png differ diff --git a/assets/images/lv/lv2.png b/assets/images/lv/lv2.png new file mode 100644 index 000000000..895653eca Binary files /dev/null and b/assets/images/lv/lv2.png differ diff --git a/assets/images/lv/lv3.png b/assets/images/lv/lv3.png new file mode 100644 index 000000000..54e08d2d7 Binary files /dev/null and b/assets/images/lv/lv3.png differ diff --git a/assets/images/lv/lv4.png b/assets/images/lv/lv4.png new file mode 100644 index 000000000..bdb5fd414 Binary files /dev/null and b/assets/images/lv/lv4.png differ diff --git a/assets/images/lv/lv5.png b/assets/images/lv/lv5.png new file mode 100644 index 000000000..6973c0a7e Binary files /dev/null and b/assets/images/lv/lv5.png differ diff --git a/assets/images/lv/lv6.png b/assets/images/lv/lv6.png new file mode 100644 index 000000000..14ba61815 Binary files /dev/null and b/assets/images/lv/lv6.png differ diff --git a/lib/common/widgets/reply_item.dart b/lib/common/widgets/reply_item.dart new file mode 100644 index 000000000..893bae18f --- /dev/null +++ b/lib/common/widgets/reply_item.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/models/video/reply/item.dart'; +import 'package:pilipala/utils/utils.dart'; + +class ReplyItem extends StatefulWidget { + ReplyItem({super.key, this.replyItem, this.isUp}); + ReplyItemModel? replyItem; + bool? isUp; + + @override + State createState() => _ReplyItemState(); +} + +class _ReplyItemState extends State { + ReplyItemModel? replyItem; + bool isUp = false; + + @override + void initState() { + super.initState(); + replyItem = widget.replyItem; + isUp = widget.isUp!; + } + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () {}, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 8, 8, 4), + child: content(context), + ), + Divider( + height: 1, + color: Theme.of(context).dividerColor.withOpacity(0.08), + ) + ], + ), + ); + } + + Widget lfAvtar() { + return Container( + margin: const EdgeInsets.only(top: 5), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.primary.withOpacity(0.03)), + ), + child: NetworkImgLayer( + src: replyItem!.member!.avatar, + width: 34, + height: 34, + type: 'avatar', + ), + ); + } + + Widget content(context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 头像、昵称 + Row( + // 两端对齐 + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + // onTap: () => + // Get.toNamed('/member/${reply.userName}', parameters: { + // 'memberAvatar': reply.avatar, + // 'heroTag': reply.userName + heroTag, + // }), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + lfAvtar(), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + replyItem!.member!.uname!, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: Theme.of(context) + .textTheme + .titleSmall! + .copyWith( + color: isUp + ? Theme.of(context).colorScheme.primary + : null, + ), + ), + const SizedBox(width: 6), + Image.asset( + 'assets/images/lv/lv${replyItem!.member!.level}.png', + height: 13, + ), + ], + ), + Text( + Utils.dateFormat(replyItem!.ctime), + style: Theme.of(context).textTheme.labelSmall!.copyWith( + color: Theme.of(context).colorScheme.outline), + ), + ], + ) + ], + ), + ), + // SizedBox( + // width: 35, + // height: 35, + // child: IconButton( + // padding: const EdgeInsets.all(2.0), + // icon: const Icon(Icons.more_horiz_outlined, size: 18.0), + // onPressed: () {}, + // ), + // ) + ], + ), + // title + Container( + margin: const EdgeInsets.only(top: 6, left: 45, right: 8), + child: SelectionArea( + child: Text( + replyItem!.content!.message!, + style: const TextStyle(height: 1.8), + ), + ), + ), + bottonAction(), + ], + ); + } + + // 感谢、回复、复制 + Widget bottonAction() { + var color = Theme.of(context).colorScheme.outline; + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // const SizedBox(width: 42), + SizedBox( + height: 35, + child: TextButton( + child: Row( + children: [ + Icon( + Icons.thumb_up_alt_outlined, + size: 16, + color: color, + ), + const SizedBox(width: 4), + Text( + replyItem!.like.toString(), + style: TextStyle( + color: color, + fontSize: + Theme.of(context).textTheme.labelSmall!.fontSize), + ), + ], + ), + onPressed: () {}, + ), + ), + const SizedBox(width: 5) + ], + ); + } +} diff --git a/lib/http/reply.dart b/lib/http/reply.dart index 1762ee4ee..880009960 100644 --- a/lib/http/reply.dart +++ b/lib/http/reply.dart @@ -14,8 +14,11 @@ class ReplyHttp { 'type': type, 'sort': 1, }); - print(res); if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; } else { Map errMap = { -400: '请求错误', diff --git a/lib/models/video/reply/config.dart b/lib/models/video/reply/config.dart new file mode 100644 index 000000000..90574f7c1 --- /dev/null +++ b/lib/models/video/reply/config.dart @@ -0,0 +1,17 @@ +class ReplyConfig { + ReplyConfig({ + this.showtopic, + this.showUpFlag, + this.readOnly, + }); + + int? showtopic; + bool? showUpFlag; + bool? readOnly; + + ReplyConfig.fromJson(Map json) { + showtopic = json['showtopic']; + showUpFlag = json['show_up_flag']; + readOnly = json['read_only']; + } +} diff --git a/lib/models/video/reply/content.dart b/lib/models/video/reply/content.dart new file mode 100644 index 000000000..f924d3f21 --- /dev/null +++ b/lib/models/video/reply/content.dart @@ -0,0 +1,23 @@ +class ReplyContent { + ReplyContent({ + this.message, + this.atNameToMid, // @的用户的mid + this.memebers, // 被@的用户List 如果有的话 + this.emote, // 表情包 如果有的话 + this.jumpUrl, + }); + + String? message; + Map? atNameToMid; + List? memebers; + Map? emote; + Map? jumpUrl; + + ReplyContent.fromJson(Map json) { + message = json['message']; + atNameToMid = json['at_name_to_mid']; + memebers = json['memebers']; + emote = json['emote']; + jumpUrl = json['jumpUrl']; + } +} diff --git a/lib/models/video/reply/data.dart b/lib/models/video/reply/data.dart new file mode 100644 index 000000000..b47ff656c --- /dev/null +++ b/lib/models/video/reply/data.dart @@ -0,0 +1,34 @@ +import 'package:pilipala/models/video/reply/item.dart'; + +import 'config.dart'; +import 'page.dart'; +import 'upper.dart'; + +class ReplyData { + ReplyData({ + this.page, + this.config, + this.replies, + this.topReplies, + this.upper, + }); + + ReplyPage? page; + ReplyConfig? config; + late List? replies; + late List? topReplies; + ReplyUpper? upper; + + ReplyData.fromJson(Map json) { + page = ReplyPage.fromJson(json['page']); + config = ReplyConfig.fromJson(json['config']); + replies = + json['replies'].map((item) => ReplyItemModel.fromJson(item)).toList(); + topReplies = json['top_replies'] != null + ? json['top_replies'] + .map((item) => ReplyItemModel.fromJson(item)) + .toList() + : []; + upper = ReplyUpper.fromJson(json['upper']); + } +} diff --git a/lib/models/video/reply/item.dart b/lib/models/video/reply/item.dart new file mode 100644 index 000000000..f8c791872 --- /dev/null +++ b/lib/models/video/reply/item.dart @@ -0,0 +1,125 @@ +import 'content.dart'; +import 'member.dart'; + +class ReplyItemModel { + ReplyItemModel({ + this.rpid, + this.oid, + this.type, + this.mid, + this.root, + this.parent, + this.dialog, + this.count, + this.floor, + this.state, + this.fansgrade, + this.attr, + this.ctime, + this.rpidStr, + this.rootStr, + this.parentStr, + this.like, + this.action, + this.member, + this.content, + this.replies, + this.assist, + this.upAction, + this.invisible, + this.replyControl, + }); + + int? rpid; + int? oid; + int? type; + int? mid; + int? root; + int? parent; + int? dialog; + int? count; + int? floor; + int? state; + int? fansgrade; + int? attr; + int? ctime; + String? rpidStr; + String? rootStr; + String? parentStr; + int? like; + int? action; + ReplyMember? member; + ReplyContent? content; + List? replies; + int? assist; + UpAction? upAction; + bool? invisible; + ReplyControl? replyControl; + + ReplyItemModel.fromJson(Map json) { + rpid = json['rpid']; + oid = json['oid']; + type = json['type']; + mid = json['mid']; + root = json['root']; + parent = json['parent']; + dialog = json['dialog']; + count = json['count']; + floor = json['floor']; + state = json['state']; + fansgrade = json['fansgrade']; + attr = json['attr']; + ctime = json['ctime']; + rpidStr = json['rpid_str']; + rootStr = json['root_str']; + parentStr = json['parent_str']; + like = json['like']; + action = json['action']; + member = ReplyMember.fromJson(json['member']); + content = ReplyContent.fromJson(json['content']); + replies = json['replies']; + assist = json['assist']; + upAction = UpAction.fromJson(json['up_action']); + invisible = json['invisible']; + replyControl = ReplyControl.fromJson(json['reply_control']); + } +} + +class UpAction { + UpAction({this.like, this.reply}); + + bool? like; + bool? reply; + + UpAction.fromJson(Map json) { + like = json['like']; + reply = json['reply']; + } +} + +class ReplyControl { + ReplyControl({ + this.upReply, + this.isUpTop, + this.entryText, + this.titleText, + this.time, + this.location, + }); + + bool? upReply; + bool? isUpTop; + String? entryText; + String? titleText; + String? time; + String? location; + + ReplyControl.fromJson(Map json) { + upReply = json['up_reply']; + isUpTop = json['is_up_top']; + entryText = json['sub_reply_entry_text']; + titleText = json['sub_reply_title_text']; + time = json['time_desc']; + location = json['location']; + } +} diff --git a/lib/models/video/reply/member.dart b/lib/models/video/reply/member.dart new file mode 100644 index 000000000..196f252b0 --- /dev/null +++ b/lib/models/video/reply/member.dart @@ -0,0 +1,55 @@ +import 'dart:convert' as convert; + +class ReplyMember { + ReplyMember({ + this.mid, + this.uname, + this.sign, + this.avatar, + this.level, + this.pendant, + this.officialVerify, + this.vip, + this.fansDetail, + }); + + String? mid; + String? uname; + String? sign; + String? avatar; + int? level; + Pendant? pendant; + Map? officialVerify; + Map? vip; + Map? fansDetail; + + ReplyMember.fromJson(Map json) { + mid = json['mid']; + uname = json['uname']; + sign = json['sign']; + avatar = json['avatar']; + level = json['level_info']['current_level']; + pendant = Pendant.fromJson(json['pendant']); + officialVerify = json['officia_vVerify']; + vip = json['vip']; + fansDetail = json['fans_detail']; + } +} + +class Pendant { + Pendant({ + this.pid, + this.name, + this.image, + }); + + int? pid; + String? name; + String? image; + + Pendant.fromJson(Map json) { + pid = json['pid']; + name = json['name']; + image = json['image']; + } +} diff --git a/lib/models/video/reply/page.dart b/lib/models/video/reply/page.dart new file mode 100644 index 000000000..771b05158 --- /dev/null +++ b/lib/models/video/reply/page.dart @@ -0,0 +1,20 @@ +class ReplyPage { + ReplyPage({ + this.num, + this.size, + this.count, + this.acount, + }); + + int? num; + int? size; + int? count; + int? acount; + + ReplyPage.fromJson(Map json) { + num = json['num']; + size = json['size']; + count = json['count']; + acount = json['acount']; + } +} diff --git a/lib/models/video/reply/top_replies.dart b/lib/models/video/reply/top_replies.dart new file mode 100644 index 000000000..f769a8341 --- /dev/null +++ b/lib/models/video/reply/top_replies.dart @@ -0,0 +1 @@ +class ReplyTop {} diff --git a/lib/models/video/reply/upper.dart b/lib/models/video/reply/upper.dart new file mode 100644 index 000000000..530513aaa --- /dev/null +++ b/lib/models/video/reply/upper.dart @@ -0,0 +1,18 @@ +import 'item.dart'; + +class ReplyUpper { + ReplyUpper({ + this.mid, + this.top, + }); + + int? mid; + ReplyItemModel? top; + + ReplyUpper.fromJson(Map json) { + mid = json['mid']; + top = json['top'] != null + ? ReplyItemModel.fromJson(json['top']) + : ReplyItemModel(); + } +} diff --git a/lib/pages/video/detail/reply/controller.dart b/lib/pages/video/detail/reply/controller.dart index d18a74b24..5a3a00e03 100644 --- a/lib/pages/video/detail/reply/controller.dart +++ b/lib/pages/video/detail/reply/controller.dart @@ -1,5 +1,6 @@ import 'package:get/get.dart'; import 'package:pilipala/http/reply.dart'; +import 'package:pilipala/models/video/reply/data.dart'; class VideoReplyController extends GetxController { // 视频aid @@ -13,5 +14,10 @@ class VideoReplyController extends GetxController { Future queryReplyList() async { var res = await ReplyHttp.replyList(oid: aid, pageNum: 1, type: 1); + if (res['status']) { + res['data'] = ReplyData.fromJson(res['data']); + print(res['data'].replies); + } + return res; } } diff --git a/lib/pages/video/detail/reply/view.dart b/lib/pages/video/detail/reply/view.dart index 13661065a..ff201b167 100644 --- a/lib/pages/video/detail/reply/view.dart +++ b/lib/pages/video/detail/reply/view.dart @@ -1,4 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/skeleton/video_card_h.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/common/widgets/reply_item.dart'; +import 'controller.dart'; class VideoReplyPanel extends StatefulWidget { const VideoReplyPanel({super.key}); @@ -8,10 +13,46 @@ class VideoReplyPanel extends StatefulWidget { } class _VideoReplyPanelState extends State { + final VideoReplyController _videoReplyController = + Get.put(VideoReplyController(), tag: Get.arguments['heroTag']); + @override Widget build(BuildContext context) { - return const SliverToBoxAdapter( - child: Text('评论'), + return FutureBuilder( + future: _videoReplyController.queryReplyList(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.data['status']) { + List replies = snapshot.data['data'].replies; + replies.addAll(snapshot.data['data'].topReplies); + // 请求成功 + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + if (index == replies.length) { + return SizedBox(height: MediaQuery.of(context).padding.bottom); + } else { + return ReplyItem( + replyItem: replies[index], + isUp: + replies[index].mid == snapshot.data['data'].upper.mid); + } + }, childCount: replies.length + 1)); + } else { + // 请求错误 + return HttpError( + errMsg: snapshot.data['msg'], + fn: () => setState(() {}), + ); + } + } else { + // 骨架屏 + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return const VideoCardHSkeleton(); + }, childCount: 5), + ); + } + }, ); } } diff --git a/pubspec.yaml b/pubspec.yaml index 6d3b671c3..7eaab1853 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -76,6 +76,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - assets/images/ + - assets/images/lv/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware