diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 656da14e..fabc511d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -34,8 +34,6 @@ PODS: - sqflite (0.0.2): - Flutter - FMDB (>= 2.7.5) - - url_launcher_ios (0.0.1): - - Flutter - volume_controller (0.0.1): - Flutter - wakelock (0.0.1): @@ -60,7 +58,6 @@ DEPENDENCIES: - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - volume_controller (from `.symlinks/plugins/volume_controller/ios`) - wakelock (from `.symlinks/plugins/wakelock/ios`) - webview_cookie_manager (from `.symlinks/plugins/webview_cookie_manager/ios`) @@ -100,8 +97,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/shared_preferences_foundation/ios" sqflite: :path: ".symlinks/plugins/sqflite/ios" - url_launcher_ios: - :path: ".symlinks/plugins/url_launcher_ios/ios" volume_controller: :path: ".symlinks/plugins/volume_controller/ios" wakelock: @@ -128,7 +123,6 @@ SPEC CHECKSUMS: share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 - url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7 diff --git a/lib/common/widgets/pull_to_refresh_header.dart b/lib/common/widgets/pull_to_refresh_header.dart new file mode 100644 index 00000000..eaf67210 --- /dev/null +++ b/lib/common/widgets/pull_to_refresh_header.dart @@ -0,0 +1,129 @@ +import 'dart:math'; +import 'dart:ui' as ui show Image; + +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart'; + +double get maxDragOffset => 100; +double hideHeight = maxDragOffset / 2.3; +double refreshHeight = maxDragOffset / 1.5; + +class PullToRefreshHeader extends StatelessWidget { + const PullToRefreshHeader( + this.info, + this.lastRefreshTime, { + this.color, + }); + + final PullToRefreshScrollNotificationInfo? info; + final DateTime? lastRefreshTime; + final Color? color; + + @override + Widget build(BuildContext context) { + final PullToRefreshScrollNotificationInfo? _info = info; + if (_info == null) { + return Container(); + } + String text = ''; + if (_info.mode == PullToRefreshIndicatorMode.armed) { + text = 'Release to refresh'; + } else if (_info.mode == PullToRefreshIndicatorMode.refresh || + _info.mode == PullToRefreshIndicatorMode.snap) { + text = 'Loading...'; + } else if (_info.mode == PullToRefreshIndicatorMode.done) { + text = 'Refresh completed.'; + } else if (_info.mode == PullToRefreshIndicatorMode.drag) { + text = 'Pull to refresh'; + } else if (_info.mode == PullToRefreshIndicatorMode.canceled) { + text = 'Cancel refresh'; + } + + final TextStyle ts = const TextStyle( + color: Colors.grey, + ).copyWith(fontSize: 14); + + final double dragOffset = info?.dragOffset ?? 0.0; + + final DateTime time = lastRefreshTime ?? DateTime.now(); + final double top = -hideHeight + dragOffset; + return Container( + height: dragOffset, + color: color ?? Colors.transparent, + // padding: EdgeInsets.only(top: dragOffset / 3), + // padding: EdgeInsets.only(bottom: 5.0), + child: Stack( + children: [ + Positioned( + left: 0.0, + right: 0.0, + top: top, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Container( + alignment: Alignment.centerRight, + child: RefreshImage(top), + margin: const EdgeInsets.only(right: 12.0), + ), + ), + Column( + children: [ + Text(text, style: ts), + Text( + 'Last updated:' + + DateFormat('yyyy-MM-dd hh:mm').format(time), + style: ts.copyWith(fontSize: 14), + ) + ], + ), + const Spacer(), + ], + ), + ) + ], + ), + ); + } +} + +class RefreshImage extends StatelessWidget { + const RefreshImage(this.top); + + final double top; + + @override + Widget build(BuildContext context) { + const double imageSize = 30; + return ExtendedImage.asset( + 'assets/flutterCandies_grey.png', + width: imageSize, + height: imageSize, + afterPaintImage: (Canvas canvas, Rect rect, ui.Image image, Paint paint) { + final double imageHeight = image.height.toDouble(); + final double imageWidth = image.width.toDouble(); + final Size size = rect.size; + final double y = + (1 - min(top / (refreshHeight - hideHeight), 1)) * imageHeight; + + canvas.drawImageRect( + image, + Rect.fromLTWH(0.0, y, imageWidth, imageHeight - y), + Rect.fromLTWH(rect.left, rect.top + y / imageHeight * size.height, + size.width, (imageHeight - y) / imageHeight * size.height), + Paint() + ..colorFilter = + const ColorFilter.mode(Color(0xFFea5504), BlendMode.srcIn) + ..isAntiAlias = false + ..filterQuality = FilterQuality.low, + ); + + //canvas.restore(); + }, + ); + } +} diff --git a/lib/common/widgets/stat/danmu.dart b/lib/common/widgets/stat/danmu.dart index 55a3b491..67d929f1 100644 --- a/lib/common/widgets/stat/danmu.dart +++ b/lib/common/widgets/stat/danmu.dart @@ -12,13 +12,17 @@ class StatDanMu extends StatelessWidget { @override Widget build(BuildContext context) { - Color color = - theme == 'white' ? Colors.white : Theme.of(context).colorScheme.outline; + Map colorObject = { + 'white': Colors.white, + 'gray': Theme.of(context).colorScheme.outline, + 'black': Theme.of(context).colorScheme.onBackground.withOpacity(0.8), + }; + Color color = colorObject[theme]!; return Row( children: [ Icon( // CupertinoIcons.ellipses_bubble, - Icons.subtitles_outlined, + Icons.subtitles_sharp, size: 14, color: color, ), diff --git a/lib/common/widgets/stat/view.dart b/lib/common/widgets/stat/view.dart index 55d8a339..efd35b03 100644 --- a/lib/common/widgets/stat/view.dart +++ b/lib/common/widgets/stat/view.dart @@ -12,13 +12,17 @@ class StatView extends StatelessWidget { @override Widget build(BuildContext context) { - Color color = - theme == 'white' ? Colors.white : Theme.of(context).colorScheme.outline; + Map colorObject = { + 'white': Colors.white, + 'gray': Theme.of(context).colorScheme.outline, + 'black': Theme.of(context).colorScheme.onBackground.withOpacity(0.8), + }; + Color color = colorObject[theme]!; return Row( children: [ Icon( // CupertinoIcons.play_rectangle, - Icons.play_circle_outlined, + Icons.play_circle_fill_outlined, size: 13, color: color, ), diff --git a/lib/common/widgets/video_card_v.dart b/lib/common/widgets/video_card_v.dart index 3d4e17b8..ac8715cb 100644 --- a/lib/common/widgets/video_card_v.dart +++ b/lib/common/widgets/video_card_v.dart @@ -80,22 +80,22 @@ class VideoCardV extends StatelessWidget { height: maxHeight, ), ), - if (videoItem.stat.view is int && - videoItem.stat.danmaku is int) - Positioned( - left: 0, - right: 0, - bottom: 0, - child: AnimatedOpacity( - opacity: 1, - duration: const Duration(milliseconds: 200), - child: VideoStat( - view: videoItem.stat.view, - danmaku: videoItem.stat.danmaku, - duration: videoItem.duration, - ), - ), - ), + // if (videoItem.stat.view is int && + // videoItem.stat.danmaku is int) + // Positioned( + // left: 0, + // right: 0, + // bottom: 0, + // child: AnimatedOpacity( + // opacity: 1, + // duration: const Duration(milliseconds: 200), + // child: VideoStat( + // view: videoItem.stat.view, + // danmaku: videoItem.stat.danmaku, + // duration: videoItem.duration, + // ), + // ), + // ), ], ); }), @@ -118,7 +118,7 @@ class VideoContent extends StatelessWidget { return Expanded( child: Padding( // 多列 - padding: const EdgeInsets.fromLTRB(4, 5, 6, 8), + padding: const EdgeInsets.fromLTRB(4, 5, 6, 12), // 单列 // padding: const EdgeInsets.fromLTRB(14, 10, 4, 8), child: Column( @@ -136,71 +136,84 @@ class VideoContent extends StatelessWidget { maxLines: Get.find().crossAxisCount, overflow: TextOverflow.ellipsis, ), - SizedBox( - height: 18, - child: Row( - children: [ - if (videoItem.rcmdReason != null && - videoItem.rcmdReason.content != '') ...[ - Container( - padding: const EdgeInsets.fromLTRB(3, 1, 3, 1), - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .primaryContainer - .withOpacity(0.6), - borderRadius: BorderRadius.circular(3)), - child: Text( - videoItem.rcmdReason.content, - style: TextStyle( - fontSize: - Theme.of(context).textTheme.labelSmall!.fontSize, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - const SizedBox(width: 4) - ] else if (videoItem.isFollowed == 1) ...[ - Container( - padding: const EdgeInsets.fromLTRB(3, 1, 3, 1), - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .primaryContainer - .withOpacity(0.6), - borderRadius: BorderRadius.circular(3)), - child: Text( - '已关注', - style: TextStyle( - fontSize: - Theme.of(context).textTheme.labelSmall!.fontSize, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - const SizedBox(width: 4) - ], - Expanded( - child: LayoutBuilder(builder: - (BuildContext context, BoxConstraints constraints) { - return SizedBox( - width: constraints.maxWidth, - child: Text( - videoItem.owner.name, - maxLines: 1, - style: TextStyle( - fontSize: Theme.of(context) - .textTheme - .labelMedium! - .fontSize, - color: Theme.of(context).colorScheme.outline, - ), - ), - ); - }), - ), - ], - ), + // SizedBox( + // height: 18, + // child: Row( + // children: [ + // if (videoItem.rcmdReason != null && + // videoItem.rcmdReason.content != '') ...[ + // Container( + // padding: const EdgeInsets.fromLTRB(3, 1, 3, 1), + // decoration: BoxDecoration( + // color: Theme.of(context) + // .colorScheme + // .primaryContainer + // .withOpacity(0.6), + // borderRadius: BorderRadius.circular(3)), + // child: Text( + // videoItem.rcmdReason.content, + // style: TextStyle( + // fontSize: + // Theme.of(context).textTheme.labelSmall!.fontSize, + // color: Theme.of(context).colorScheme.primary, + // ), + // ), + // ), + // const SizedBox(width: 4) + // ] else if (videoItem.isFollowed == 1) ...[ + // Container( + // padding: const EdgeInsets.fromLTRB(3, 1, 3, 1), + // decoration: BoxDecoration( + // color: Theme.of(context) + // .colorScheme + // .primaryContainer + // .withOpacity(0.6), + // borderRadius: BorderRadius.circular(3)), + // child: Text( + // '已关注', + // style: TextStyle( + // fontSize: + // Theme.of(context).textTheme.labelSmall!.fontSize, + // color: Theme.of(context).colorScheme.primary, + // ), + // ), + // ), + // const SizedBox(width: 4) + // ], + // Expanded( + // child: LayoutBuilder(builder: + // (BuildContext context, BoxConstraints constraints) { + // return SizedBox( + // width: constraints.maxWidth, + // child: Text( + // videoItem.owner.name, + // maxLines: 1, + // style: TextStyle( + // fontSize: Theme.of(context) + // .textTheme + // .labelMedium! + // .fontSize, + // color: Theme.of(context).colorScheme.outline, + // ), + // ), + // ); + // }), + // ), + // ], + // ), + // ), + Row( + children: [ + StatView( + theme: 'black', + view: videoItem.stat.view, + ), + const SizedBox(width: 6), + StatDanMu( + theme: 'black', + danmu: videoItem.stat.danmaku, + ), + ], ), ], ), diff --git a/lib/http/api.dart b/lib/http/api.dart index b8805cc4..e4356840 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -203,4 +203,19 @@ class Api { // 用户名片信息 static const String memberCardInfo = '/x/web-interface/card'; + + // 用户投稿 + // https://api.bilibili.com/x/space/wbi/arc/search? + // mid=85754245& + // ps=30& + // tid=0& + // pn=1& + // keyword=& + // order=pubdate& + // platform=web& + // web_location=1550101& + // order_avoided=true& + // w_rid=d893cf98a4e010cf326373194a648360& + // wts=1689767832 + static const String memberArchive = '/x/space/wbi/arc/search'; } diff --git a/lib/http/member.dart b/lib/http/member.dart index d9f33bcc..63442b04 100644 --- a/lib/http/member.dart +++ b/lib/http/member.dart @@ -1,9 +1,23 @@ import 'package:pilipala/http/index.dart'; +import 'package:pilipala/models/member/archive.dart'; import 'package:pilipala/models/member/info.dart'; +import 'package:pilipala/utils/wbi_sign.dart'; class MemberHttp { - static Future memberInfo({String? params}) async { - var res = await Request().get(Api.memberInfo + params!); + static Future memberInfo({ + int? mid, + String token = '', + }) async { + Map params = await WbiSign().makSign({ + 'mid': mid, + 'token': token, + 'platform': 'web', + 'web_location': 1550101, + }); + var res = await Request().get( + Api.memberInfo, + data: params, + ); if (res.data['code'] == 0) { return { 'status': true, @@ -44,4 +58,42 @@ class MemberHttp { }; } } + + static Future memberArchive({ + int? mid, + int ps = 30, + int tid = 0, + int? pn, + String keyword = '', + String order = 'pubdate', + bool orderAvoided = true, + }) async { + Map params = await WbiSign().makSign({ + 'mid': mid, + 'ps': ps, + 'tid': tid, + 'pn': pn, + 'keyword': keyword, + 'order': order, + 'platform': 'web', + 'web_location': 1550101, + 'order_avoided': orderAvoided + }); + var res = await Request().get( + Api.memberArchive, + data: params, + ); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': MemberArchiveDataModel.fromJson(res.data['data']) + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } } diff --git a/lib/http/reply.dart b/lib/http/reply.dart index 51c9b8fe..aec1f96b 100644 --- a/lib/http/reply.dart +++ b/lib/http/reply.dart @@ -23,7 +23,7 @@ class ReplyHttp { Map errMap = { -400: '请求错误', -404: '无此项', - 12002: '评论区已关闭', + 12002: '当前页面评论功能已关闭"', 12009: '评论主体的type不合法', 12061: 'UP主已关闭评论区', }; @@ -48,6 +48,7 @@ class ReplyHttp { 'pn': pageNum, 'type': type, 'sort': 1, + 'csrf': await Request.getCsrf(), }); if (res.data['code'] == 0) { return { diff --git a/lib/models/common/reply_type.dart b/lib/models/common/reply_type.dart index a6e8bdb1..eef9c202 100644 --- a/lib/models/common/reply_type.dart +++ b/lib/models/common/reply_type.dart @@ -4,6 +4,8 @@ enum ReplyType { video, // 话题 topic, + // + unset2, // 活动 activity, // 小视频 diff --git a/lib/models/member/archive.dart b/lib/models/member/archive.dart new file mode 100644 index 00000000..5d2ea77e --- /dev/null +++ b/lib/models/member/archive.dart @@ -0,0 +1,164 @@ +class MemberArchiveDataModel { + MemberArchiveDataModel({ + this.list, + this.page, + }); + + ArchiveListModel? list; + Map? page; + + MemberArchiveDataModel.fromJson(Map json) { + list = ArchiveListModel.fromJson(json['list']); + page = json['page']; + } +} + +class ArchiveListModel { + ArchiveListModel({ + this.tlist, + this.vlist, + }); + + Map? tlist; + List? vlist; + + ArchiveListModel.fromJson(Map json) { + tlist = json['tlist'] != null + ? Map.from(json['tlist']).map((k, v) => + MapEntry(k, TListItemModel.fromJson(v))) + : {}; + vlist = json['vlist'] + .map((e) => VListItemModel.fromJson(e)) + .toList(); + } +} + +class TListItemModel { + TListItemModel({ + this.tid, + this.count, + this.name, + }); + + int? tid; + int? count; + String? name; + + TListItemModel.fromJson(Map json) { + tid = json['tid']; + count = json['count']; + name = json['name']; + } +} + +class VListItemModel { + VListItemModel({ + this.comment, + this.typeid, + this.play, + this.pic, + this.subtitle, + this.description, + this.copyright, + this.title, + this.review, + this.author, + this.mid, + this.created, + this.pubdate, + this.length, + this.duration, + this.videoReview, + this.aid, + this.bvid, + this.cid, + this.hideClick, + this.isChargingSrc, + this.rcmdReason, + this.owner, + }); + + int? comment; + int? typeid; + int? play; + String? pic; + String? subtitle; + String? description; + String? copyright; + String? title; + int? review; + String? author; + int? mid; + int? created; + int? pubdate; + String? length; + String? duration; + int? videoReview; + int? aid; + String? bvid; + int? cid; + bool? hideClick; + bool? isChargingSrc; + Stat? stat; + String? rcmdReason; + Owner? owner; + + VListItemModel.fromJson(Map json) { + comment = json['comment']; + typeid = json['typeid']; + play = json['play']; + pic = json['pic']; + subtitle = json['subtitle']; + description = json['description']; + copyright = json['copyright']; + title = json['title']; + review = json['review']; + author = json['author']; + mid = json['mid']; + created = json['created']; + pubdate = json['created']; + length = json['length']; + duration = json['length']; + videoReview = json['video_review']; + aid = json['aid']; + bvid = json['bvid']; + cid = null; + hideClick = json['hide_click']; + isChargingSrc = json['is_charging_arc']; + stat = Stat.fromJson(json); + rcmdReason = null; + owner = Owner.fromJson(json); + } +} + +class Stat { + Stat({ + this.view, + this.danmaku, + }); + + int? view; + int? danmaku; + + Stat.fromJson(Map json) { + view = json["play"]; + danmaku = json['comment']; + } +} + +class Owner { + Owner({ + this.mid, + this.name, + this.face, + }); + int? mid; + String? name; + String? face; + + Owner.fromJson(Map json) { + mid = json["mid"]; + name = json["author"]; + face = ''; + } +} diff --git a/lib/models/member/info.dart b/lib/models/member/info.dart index 72ee462a..789131ee 100644 --- a/lib/models/member/info.dart +++ b/lib/models/member/info.dart @@ -71,6 +71,7 @@ class LiveRoom { this.cover, this.roomId, this.roundStatus, + this.watchedShow, }); int? roomStatus; @@ -80,6 +81,7 @@ class LiveRoom { String? cover; int? roomId; int? roundStatus; + Map? watchedShow; LiveRoom.fromJson(Map json) { roomStatus = json['roomStatus']; @@ -89,5 +91,6 @@ class LiveRoom { cover = json['cover']; roomId = json['roomid']; roundStatus = json['roundStatus']; + watchedShow = json['watched_show']; } } diff --git a/lib/pages/dynamics/controller.dart b/lib/pages/dynamics/controller.dart index 87f6da06..4f291454 100644 --- a/lib/pages/dynamics/controller.dart +++ b/lib/pages/dynamics/controller.dart @@ -57,9 +57,9 @@ class DynamicsController extends GetxController { ); if (res['status']) { if (type == 'init') { - dynamicsList!.value = res['data'].items; + dynamicsList.value = res['data'].items; } else { - dynamicsList!.addAll(res['data'].items); + dynamicsList.addAll(res['data'].items); } offset = res['data'].offset; page++; @@ -69,7 +69,7 @@ class DynamicsController extends GetxController { onSelectType(value) async { dynamicsType.value = filterTypeList[value - 1]['value']; - dynamicsList!.value = [DynamicItemModel()]; + dynamicsList.value = [DynamicItemModel()]; page = 1; initialValue.value = value; await queryFollowDynamic(); @@ -128,6 +128,7 @@ class DynamicsController extends GetxController { 'mid': author.mid, 'face': author.face, 'roomid': liveRcmd.roomId, + 'watched_show': liveRcmd.watchedShow, }); Get.toNamed('/liveRoom?roomid=${liveItem.roomId}', arguments: { 'liveItem': liveItem, @@ -151,7 +152,7 @@ class DynamicsController extends GetxController { onSelectUp(mid) async { dynamicsType.value = DynamicsType.values[0]; - dynamicsList!.value = [DynamicItemModel()]; + dynamicsList.value = [DynamicItemModel()]; page = 1; queryFollowDynamic(); } diff --git a/lib/pages/dynamics/deatil/view.dart b/lib/pages/dynamics/deatil/view.dart index 17df4cec..43c40d57 100644 --- a/lib/pages/dynamics/deatil/view.dart +++ b/lib/pages/dynamics/deatil/view.dart @@ -4,9 +4,11 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/skeleton/video_reply.dart'; import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/models/common/reply_type.dart'; import 'package:pilipala/pages/dynamics/deatil/index.dart'; import 'package:pilipala/pages/dynamics/widgets/author_panel.dart'; import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart'; +import 'package:pilipala/pages/video/detail/replyReply/index.dart'; import '../widgets/dynamic_panel.dart'; @@ -25,19 +27,20 @@ class _DynamicDetailPageState extends State { final ScrollController scrollController = ScrollController(); bool _visibleTitle = false; String? action; + // 回复类型 + late int type; @override void initState() { super.initState(); int oid = 0; - int type = 0; + // floor 1原创 2转发 if (Get.arguments['floor'] == 1) { oid = int.parse(Get.arguments['item'].basic!['comment_id_str']); - type = Get.arguments['item'].basic!['comment_type']; } else { oid = Get.arguments['item'].modules.moduleDynamic.major.draw.id; - type = 11; } + type = Get.arguments['item'].basic!['comment_type']; action = Get.arguments.containsKey('action') ? Get.arguments['action'] : null; _dynamicDetailController = Get.put(DynamicDetailController(oid, type)); @@ -68,6 +71,26 @@ class _DynamicDetailPageState extends State { } } + void replyReply(replyItem, paddingTop) { + int oid = replyItem.replies!.first.oid; + int rpid = replyItem.rpid!; + Get.to( + () => Scaffold( + appBar: AppBar( + title: const Text('评论详情'), + centerTitle: false, + ), + body: VideoReplyReplyPanel( + oid: oid, + rpid: rpid, + paddingTop: paddingTop, + source: 'dynamic', + replyType: ReplyType.values[type], + ), + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -189,10 +212,14 @@ class _DynamicDetailPageState extends State { ); } else { return ReplyItem( - replyItem: _dynamicDetailController! - .replyList[index], - showReplyRow: true, - replyLevel: '1'); + replyItem: + _dynamicDetailController!.replyList[index], + showReplyRow: true, + replyLevel: '1', + replyReply: (replyItem, paddingTop) => + replyReply(replyItem, paddingTop), + replyType: ReplyType.album, + ); } }, childCount: @@ -212,7 +239,7 @@ class _DynamicDetailPageState extends State { return SliverList( delegate: SliverChildBuilderDelegate((context, index) { return const VideoReplySkeleton(); - }, childCount: 5), + }, childCount: 8), ); } }, diff --git a/lib/pages/dynamics/view.dart b/lib/pages/dynamics/view.dart index a0eaae4d..0319f59b 100644 --- a/lib/pages/dynamics/view.dart +++ b/lib/pages/dynamics/view.dart @@ -234,7 +234,7 @@ class _DynamicsPageState extends State Map data = snapshot.data; if (data['status']) { List list = - _dynamicsController.dynamicsList!; + _dynamicsController.dynamicsList; return Obx( () => list.length == 1 ? skeleton() diff --git a/lib/pages/dynamics/widgets/additional_panel.dart b/lib/pages/dynamics/widgets/additional_panel.dart index 0396f219..d2015aee 100644 --- a/lib/pages/dynamics/widgets/additional_panel.dart +++ b/lib/pages/dynamics/widgets/additional_panel.dart @@ -23,7 +23,7 @@ Widget addWidget(item, context, type, {floor = 1}) { onTap: () {}, child: Container( padding: - const EdgeInsets.only(left: 15, top: 10, right: 15, bottom: 8), + const EdgeInsets.only(left: 12, top: 8, right: 12, bottom: 8), color: bgColor, child: Row( children: [ @@ -60,90 +60,104 @@ Widget addWidget(item, context, type, {floor = 1}) { ), ); case 'ADDITIONAL_TYPE_RESERVE': - return InkWell( - onTap: () {}, - child: Container( - margin: const EdgeInsets.only(top: 8), - padding: - const EdgeInsets.only(left: 15, top: 12, right: 15, bottom: 10), - color: bgColor, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(dynamicProperty[type].title), - Text.rich(TextSpan( - style: TextStyle( - color: Theme.of(context).colorScheme.outline, - fontSize: Theme.of(context) - .textTheme - .labelMedium! - .fontSize), - children: [ - TextSpan(text: dynamicProperty[type].desc1['text']), - TextSpan(text: dynamicProperty[type].desc2['text']), - ])) - ], - ), - // TextButton(onPressed: () {}, child: Text('123')) - ], + return Padding( + padding: const EdgeInsets.only(top: 8), + child: InkWell( + onTap: () {}, + child: Container( + width: double.infinity, + padding: + const EdgeInsets.only(left: 12, top: 10, right: 12, bottom: 10), + color: bgColor, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + dynamicProperty[type].title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 1), + Text.rich( + TextSpan( + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + fontSize: + Theme.of(context).textTheme.labelMedium!.fontSize), + children: [ + TextSpan(text: dynamicProperty[type].desc1['text']), + const TextSpan(text: ' '), + TextSpan(text: dynamicProperty[type].desc2['text']), + ], + ), + ) + ], + ), + // TextButton(onPressed: () {}, child: Text('123')) ), ), ); case 'ADDITIONAL_TYPE_GOODS': - return Container( - margin: const EdgeInsets.only(top: 6), - padding: const EdgeInsets.only(left: 15, top: 10, right: 15, bottom: 8), - decoration: BoxDecoration( - color: bgColor, - borderRadius: BorderRadius.all(Radius.circular(6)), - ), - child: Row( - children: [ - NetworkImgLayer( - width: 75, - height: 75, - src: dynamicProperty[type].items.first.cover, - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, + return Padding( + padding: const EdgeInsets.only(top: 6), + child: InkWell( + onTap: () {}, + child: Container( + padding: + const EdgeInsets.only(left: 12, top: 8, right: 12, bottom: 8), + decoration: BoxDecoration( + color: bgColor, + borderRadius: const BorderRadius.all(Radius.circular(6)), + ), + child: Row( children: [ - Text( - dynamicProperty[type].items.first.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, + NetworkImgLayer( + width: 75, + height: 75, + src: dynamicProperty[type].items.first.cover, ), - Text( - dynamicProperty[type].items.first.brief, - maxLines: 1, - style: TextStyle( - color: Theme.of(context).colorScheme.outline, - fontSize: - Theme.of(context).textTheme.labelMedium!.fontSize, - ), - ), - const SizedBox(height: 2), - Text( - dynamicProperty[type].items.first.price, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + dynamicProperty[type].items.first.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + dynamicProperty[type].items.first.brief, + maxLines: 1, + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + fontSize: Theme.of(context) + .textTheme + .labelMedium! + .fontSize, + ), + ), + const SizedBox(height: 2), + Text( + dynamicProperty[type].items.first.price, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + ), + ), + ], ), ), ], ), ), - ], - ), - ); + )); case 'ADDITIONAL_TYPE_MATCH': return SizedBox(); case 'ADDITIONAL_TYPE_COMMON': return SizedBox(); + case 'ADDITIONAL_TYPE_VOTE': + return SizedBox(); default: return Text('11'); } diff --git a/lib/pages/dynamics/widgets/content_panel.dart b/lib/pages/dynamics/widgets/content_panel.dart index 872be803..34324d2e 100644 --- a/lib/pages/dynamics/widgets/content_panel.dart +++ b/lib/pages/dynamics/widgets/content_panel.dart @@ -7,24 +7,34 @@ Widget content(item, context, source) { TextStyle authorStyle = TextStyle(color: Theme.of(context).colorScheme.primary); return Container( - width: double.infinity, - padding: const EdgeInsets.fromLTRB(12, 0, 12, 6), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (item.modules.moduleDynamic.topic != null) ...[ - GestureDetector( - child: Text( - '#${item.modules.moduleDynamic.topic.name}', - style: authorStyle, - ), + width: double.infinity, + padding: const EdgeInsets.fromLTRB(12, 0, 12, 6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (item.modules.moduleDynamic.topic != null) ...[ + GestureDetector( + child: Text( + '#${item.modules.moduleDynamic.topic.name}', + style: authorStyle, ), - ], - Text.rich( - richNode(item, context), - maxLines: source == 'detail' ? 999 : 3, - overflow: TextOverflow.ellipsis, ), ], - )); + IgnorePointer( + // 禁用SelectableRegion的触摸交互功能 + ignoring: source == 'detail' ? false : true, + child: SelectableRegion( + magnifierConfiguration: const TextMagnifierConfiguration(), + focusNode: FocusNode(), + selectionControls: MaterialTextSelectionControls(), + child: Text.rich( + richNode(item, context), + maxLines: source == 'detail' ? 999 : 3, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + ); } diff --git a/lib/pages/dynamics/widgets/rich_node_panel.dart b/lib/pages/dynamics/widgets/rich_node_panel.dart index e617fac2..f13c6548 100644 --- a/lib/pages/dynamics/widgets/rich_node_panel.dart +++ b/lib/pages/dynamics/widgets/rich_node_panel.dart @@ -64,7 +64,12 @@ InlineSpan richNode(item, context) { WidgetSpan( alignment: PlaceholderAlignment.middle, child: GestureDetector( - onTap: () {}, + onTap: () { + Get.toNamed( + '/webview', + parameters: {'url': i.origText, 'type': 'url', 'pageTitle': ''}, + ); + }, child: Text( i.text, style: authorStyle, @@ -79,7 +84,18 @@ InlineSpan richNode(item, context) { WidgetSpan( alignment: PlaceholderAlignment.middle, child: GestureDetector( - onTap: () {}, + onTap: () { + String dynamicId = item.basic['comment_id_str']; + Get.toNamed( + '/webview', + parameters: { + 'url': + 'https://t.bilibili.com/vote/h5/index/#/result?vote_id=${i.rid}&dynamic_id=${dynamicId}&isWeb=1', + 'type': 'vote', + 'pageTitle': '投票' + }, + ); + }, child: Text( '投票:${i.text}', style: authorStyle, diff --git a/lib/pages/dynamics/widgets/up_panel.dart b/lib/pages/dynamics/widgets/up_panel.dart index 40ddfc45..c6350249 100644 --- a/lib/pages/dynamics/widgets/up_panel.dart +++ b/lib/pages/dynamics/widgets/up_panel.dart @@ -7,6 +7,7 @@ import 'package:pilipala/models/dynamics/up.dart'; import 'package:pilipala/models/live/item.dart'; import 'package:pilipala/pages/dynamics/controller.dart'; import 'package:pilipala/utils/storage.dart'; +import 'package:pilipala/utils/utils.dart'; class UpPanel extends StatefulWidget { FollowUpModel? upData; @@ -92,8 +93,10 @@ class _UpPanelState extends State { child: Center( child: Text( '全部', - style: - TextStyle(color: Theme.of(context).primaryColor), + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer), ), ), ), @@ -146,6 +149,11 @@ class _UpPanelState extends State { ); } }, + onLongPress: () { + String heroTag = Utils.makeHeroTag(data.mid); + Get.toNamed('/member?mid=${data.mid}', + arguments: {'face': data.face, 'heroTag': heroTag}); + }, child: Padding( padding: itemPadding, child: AnimatedOpacity( diff --git a/lib/pages/home/view.dart b/lib/pages/home/view.dart index 3c34d2c7..f27b6a50 100644 --- a/lib/pages/home/view.dart +++ b/lib/pages/home/view.dart @@ -29,7 +29,7 @@ class _HomePageState extends State appBar: AppBar( titleSpacing: 0, title: Padding( - padding: const EdgeInsets.only(left: 8, right: 8), + padding: const EdgeInsets.only(left: 12, right: 12, bottom: 8), child: Stack( children: [ const Align( @@ -79,13 +79,12 @@ class _HomePageState extends State indicatorPadding: const EdgeInsets.symmetric( horizontal: 4, vertical: 5), indicator: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, + color: Theme.of(context).colorScheme.primary, borderRadius: const BorderRadius.all(Radius.circular(20)), ), indicatorSize: TabBarIndicatorSize.tab, - labelColor: - Theme.of(context).colorScheme.onSecondaryContainer, + labelColor: Theme.of(context).colorScheme.onPrimary, labelStyle: const TextStyle(fontSize: 13), dividerColor: Colors.transparent, unselectedLabelColor: diff --git a/lib/pages/liveRoom/controller.dart b/lib/pages/liveRoom/controller.dart index b910bc6e..42e28e10 100644 --- a/lib/pages/liveRoom/controller.dart +++ b/lib/pages/liveRoom/controller.dart @@ -10,11 +10,14 @@ class LiveRoomController extends GetxController { late int roomId; var liveItem; late String heroTag; + double volume = 0.0; + // 静音状态 + RxBool volumeOff = false.obs; MeeduPlayerController meeduPlayerController = MeeduPlayerController( colorTheme: Theme.of(Get.context!).colorScheme.primary, pipEnabled: true, - controlsStyle: ControlsStyle.youtube, + controlsStyle: ControlsStyle.live, enabledButtons: const EnabledButtons(pip: true), ); @@ -45,6 +48,7 @@ class LiveRoomController extends GetxController { ), autoplay: true, ); + volume = meeduPlayerController.volume.value; } Future queryLiveInfo() async { @@ -59,4 +63,18 @@ class LiveRoomController extends GetxController { playerInit(videoUrl); } } + + void setVolumn(value) { + if (value == 0) { + // 设置音量 + volumeOff.value = false; + meeduPlayerController.setVolume(volume); + } else { + // 取消音量 + volume = value; + volumeOff.value = true; + meeduPlayerController.setVolume(0); + } + print('🌹:${volumeOff.value}'); + } } diff --git a/lib/pages/liveRoom/view.dart b/lib/pages/liveRoom/view.dart index 556803e5..9bfd079b 100644 --- a/lib/pages/liveRoom/view.dart +++ b/lib/pages/liveRoom/view.dart @@ -69,13 +69,23 @@ class _LiveRoomPageState extends State { _liveRoomController.liveItem.uname, style: const TextStyle(fontSize: 14), ), - const SizedBox(height: 3), - Text(_liveRoomController.liveItem.title, - style: const TextStyle(fontSize: 12)), + const SizedBox(height: 1), + if (_liveRoomController.liveItem.watchedShow != null) + Text( + _liveRoomController.liveItem.watchedShow['text_large'] ?? + '', + style: const TextStyle(fontSize: 12)), ], - ) + ), ], ), + actions: [ + SizedBox( + height: 34, + child: ElevatedButton(onPressed: () {}, child: const Text('关注')), + ), + const SizedBox(width: 12), + ], ), body: Column( children: [ @@ -86,6 +96,36 @@ class _LiveRoomPageState extends State { AspectRatio( aspectRatio: 16 / 9, child: MeeduVideoPlayer( + header: (BuildContext context, + MeeduPlayerController _meeduPlayerController, + Responsive) { + return AppBar( + backgroundColor: Colors.transparent, + primary: false, + elevation: 0, + scrolledUnderElevation: 0, + foregroundColor: Colors.white, + automaticallyImplyLeading: false, + centerTitle: false, + title: Text(_liveRoomController.liveItem.title, + style: const TextStyle(fontSize: 12)), + actions: [ + SizedBox( + width: 38, + height: 38, + child: IconButton( + onPressed: () => + _meeduPlayerController.enterPip(context), + icon: const Icon( + Icons.branding_watermark_outlined, + size: 19, + ), + ), + ), + const SizedBox(width: 12) + ], + ); + }, controller: _meeduPlayerController!, ), ), @@ -110,9 +150,6 @@ class _LiveRoomPageState extends State { Container( height: 45, padding: const EdgeInsets.only(left: 12, right: 12), - child: Row(children: [ - Text(_liveRoomController.liveItem.watchedShow['text_large']), - ]), decoration: BoxDecoration( color: Theme.of(context).colorScheme.background, border: Border( @@ -120,6 +157,56 @@ class _LiveRoomPageState extends State { color: Theme.of(context).dividerColor.withOpacity(0.1)), ), ), + child: Row(children: [ + // SizedBox( + // width: 38, + // height: 38, + // child: IconButton( + // onPressed: () {}, + // icon: const Icon( + // Icons.subtitles_outlined, + // size: 21, + // ), + // ), + // ), + const Spacer(), + // SizedBox( + // width: 38, + // height: 38, + // child: IconButton( + // onPressed: () {}, + // icon: const Icon( + // Icons.hd_outlined, + // size: 20, + // ), + // ), + // ), + SizedBox( + width: 38, + height: 38, + child: IconButton( + onPressed: () => _liveRoomController + .setVolumn(_meeduPlayerController!.volume.value), + icon: Obx(() => Icon( + _liveRoomController.volumeOff.value + ? Icons.volume_off_outlined + : Icons.volume_up_outlined, + size: 21, + )), + ), + ), + SizedBox( + width: 38, + height: 38, + child: IconButton( + onPressed: () => + _meeduPlayerController!.goToFullscreen(context), + icon: const Icon( + Icons.fullscreen, + ), + ), + ), + ]), ), ], ), diff --git a/lib/pages/member/archive/controller.dart b/lib/pages/member/archive/controller.dart new file mode 100644 index 00000000..52400893 --- /dev/null +++ b/lib/pages/member/archive/controller.dart @@ -0,0 +1,22 @@ +import 'package:get/get.dart'; +import 'package:pilipala/http/member.dart'; + +class ArchiveController extends GetxController { + int? mid; + int pn = 1; + + @override + void onInit() { + super.onInit(); + mid = int.parse(Get.parameters['mid']!); + } + + // 获取用户投稿 + Future getMemberArchive() async { + var res = await MemberHttp.memberArchive(mid: mid, pn: pn); + if (res['status']) { + pn += 1; + } + return res; + } +} diff --git a/lib/pages/member/archive/index.dart b/lib/pages/member/archive/index.dart new file mode 100644 index 00000000..8be45429 --- /dev/null +++ b/lib/pages/member/archive/index.dart @@ -0,0 +1,4 @@ +library archive_panel; + +export './controller.dart'; +export 'index.dart'; diff --git a/lib/pages/member/archive/view.dart b/lib/pages/member/archive/view.dart new file mode 100644 index 00000000..4ec77775 --- /dev/null +++ b/lib/pages/member/archive/view.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:loading_more_list/loading_more_list.dart'; +import 'package:pilipala/common/widgets/pull_to_refresh_header.dart'; +import 'package:pilipala/common/widgets/video_card_h.dart'; +import 'package:pilipala/models/member/archive.dart'; +import 'package:pilipala/pages/member/archive/index.dart'; +import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart'; + +class ArchivePanel extends StatefulWidget { + const ArchivePanel({super.key}); + + @override + State createState() => _ArchivePanelState(); +} + +class _ArchivePanelState extends State + with AutomaticKeepAliveClientMixin { + DateTime lastRefreshTime = DateTime.now(); + late final LoadMoreListSource source = LoadMoreListSource(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + return PullToRefreshNotification( + onRefresh: () async { + await Future.delayed(const Duration(seconds: 1)); + return true; + }, + maxDragOffset: 50, + child: GlowNotificationWidget( + Column( + children: [ + // 下拉刷新指示器 + PullToRefreshContainer( + (PullToRefreshScrollNotificationInfo? info) { + return PullToRefreshHeader(info, lastRefreshTime); + }, + ), + const SizedBox(height: 4), + Expanded( + child: LoadingMoreList( + ListConfig( + sourceList: source, + itemBuilder: + (BuildContext c, VListItemModel item, int index) { + return VideoCardH(videoItem: item); + }, + indicatorBuilder: (context, status) { + return const Center(child: Text('加载中')); + }, + ), + ), + ) + ], + ), + showGlowLeading: false, + ), + ); + } +} + +class LoadMoreListSource extends LoadingMoreBase { + final ArchiveController _archiveController = Get.put(ArchiveController()); + @override + Future loadData([bool isloadMoreAction = false]) { + return Future(() async { + var res = await _archiveController.getMemberArchive(); + if (res['status']) { + addAll(res['data'].list.vlist); + } + return true; + }); + } +} diff --git a/lib/pages/member/controller.dart b/lib/pages/member/controller.dart index 54c23698..78f172bc 100644 --- a/lib/pages/member/controller.dart +++ b/lib/pages/member/controller.dart @@ -1,6 +1,10 @@ +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/member.dart'; +import 'package:pilipala/http/video.dart'; +import 'package:pilipala/models/member/archive.dart'; import 'package:pilipala/models/member/info.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/wbi_sign.dart'; @@ -13,6 +17,8 @@ class MemberController extends GetxController { String? heroTag; Box user = GStrorage.user; late int ownerMid; + // 投稿列表 + RxList? archiveList = [VListItemModel()].obs; @override void onInit() { @@ -26,14 +32,7 @@ class MemberController extends GetxController { // 获取用户信息 Future> getInfo() async { await getMemberStat(); - String params = await WbiSign().makSign({ - 'mid': mid, - 'token': '', - 'platform': 'web', - 'web_location': 1550101, - }); - params = '?$params'; - var res = await MemberHttp.memberInfo(params: params); + var res = await MemberHttp.memberInfo(mid: mid); if (res['status']) { memberInfo.value = res['data']; } @@ -56,4 +55,43 @@ class MemberController extends GetxController { } return res; } + + // 关注/取关up + Future actionRelationMod() async { + if (user.get(UserBoxKey.userMid) == null) { + SmartDialog.showToast('账号未登录'); + return; + } + + SmartDialog.show( + useSystem: true, + animationType: SmartAnimationType.centerFade_otherSlide, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('提示'), + content: Text(memberInfo.value.isFollowed! ? '取消关注UP主?' : '关注UP主?'), + actions: [ + TextButton( + onPressed: () => SmartDialog.dismiss(), + child: const Text('点错了')), + TextButton( + onPressed: () async { + await VideoHttp.relationMod( + mid: mid, + act: memberInfo.value.isFollowed! ? 2 : 1, + reSrc: 11, + ); + memberInfo.value.isFollowed = !memberInfo.value.isFollowed!; + SmartDialog.dismiss(); + SmartDialog.showLoading(); + SmartDialog.dismiss(); + memberInfo.update((val) {}); + }, + child: const Text('确认'), + ) + ], + ); + }, + ); + } } diff --git a/lib/pages/member/view.dart b/lib/pages/member/view.dart index e1d82f08..68d752e6 100644 --- a/lib/pages/member/view.dart +++ b/lib/pages/member/view.dart @@ -1,14 +1,16 @@ import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; +import 'package:loading_more_list/loading_more_list.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/models/live/item.dart'; -import 'package:pilipala/models/user/stat.dart'; +import 'package:pilipala/pages/member/archive/view.dart'; import 'package:pilipala/pages/member/index.dart'; import 'package:pilipala/utils/utils.dart'; +import 'widgets/profile.dart'; + class MemberPage extends StatefulWidget { const MemberPage({super.key}); @@ -19,13 +21,15 @@ class MemberPage extends StatefulWidget { class _MemberPageState extends State with SingleTickerProviderStateMixin { final MemberController _memberController = Get.put(MemberController()); + Future? _futureBuilderFuture; final ScrollController _extendNestCtr = ScrollController(); late TabController _tabController; @override void initState() { super.initState(); - _tabController = TabController(length: 3, vsync: this); + _tabController = TabController(length: 3, vsync: this, initialIndex: 2); + _futureBuilderFuture = _memberController.getInfo(); } @override @@ -90,7 +94,7 @@ class _MemberPageState extends State Padding( padding: const EdgeInsets.only(left: 18, right: 18), child: FutureBuilder( - future: _memberController.getInfo(), + future: _futureBuilderFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { @@ -104,8 +108,7 @@ class _MemberPageState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ - profile( - _memberController.memberInfo.value), + profile(_memberController), const SizedBox(height: 14), Row( children: [ @@ -232,7 +235,8 @@ class _MemberPageState extends State } } else { // 骨架屏 - return profile(null, loadingStatus: true); + return profile(_memberController, + loadingStatus: true); } }, ), @@ -249,10 +253,10 @@ class _MemberPageState extends State onlyOneScrollInBody: true, body: Column( children: [ - Container( + SizedBox( width: double.infinity, height: 50, - child: TabBar(controller: _tabController, tabs: [ + child: TabBar(controller: _tabController, tabs: const [ Tab(text: '主页'), Tab(text: '动态'), Tab(text: '投稿'), @@ -261,10 +265,10 @@ class _MemberPageState extends State Expanded( child: TabBarView( controller: _tabController, - children: [ + children: const [ Text('主页'), Text('动态'), - Text('投稿'), + ArchivePanel(), ], )) ], @@ -272,186 +276,4 @@ class _MemberPageState extends State ), ); } - - Widget profile(memberInfo, {loadingStatus = false}) { - return Padding( - padding: EdgeInsets.only(top: 3 * MediaQuery.of(context).padding.top), - child: Row( - children: [ - Hero( - tag: _memberController.heroTag!, - child: Stack( - children: [ - NetworkImgLayer( - width: 90, - height: 90, - type: 'avatar', - src: !loadingStatus - ? memberInfo.face - : _memberController.face, - ), - if (!loadingStatus && - memberInfo.liveRoom != null && - memberInfo.liveRoom.liveStatus == 1) - Positioned( - bottom: 0, - left: 14, - child: GestureDetector( - onTap: () { - LiveItemModel liveItem = LiveItemModel.fromJson({ - 'title': memberInfo.liveRoom.title, - 'uname': memberInfo.name, - 'face': memberInfo.face, - 'roomid': memberInfo.liveRoom.roomId, - }); - Get.toNamed( - '/liveRoom?roomid=${memberInfo.liveRoom.roomId}', - arguments: {'liveItem': liveItem}, - ); - }, - child: Container( - padding: const EdgeInsets.fromLTRB(6, 2, 6, 2), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: - const BorderRadius.all(Radius.circular(10)), - ), - child: Row(children: [ - Image.asset( - 'assets/images/live.gif', - height: 10, - ), - Text( - ' 直播中', - style: TextStyle( - color: Colors.white, - fontSize: Theme.of(context) - .textTheme - .labelSmall! - .fontSize), - ) - ]), - ), - ), - ) - ], - )), - const SizedBox(width: 12), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(left: 10, right: 10), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Column( - children: [ - Text( - !loadingStatus - ? _memberController.userStat!['following'] - .toString() - : '-', - style: const TextStyle(fontWeight: FontWeight.bold), - ), - Text( - '关注', - style: TextStyle( - fontSize: Theme.of(context) - .textTheme - .labelMedium! - .fontSize), - ) - ], - ), - Column( - children: [ - Text( - !loadingStatus - ? Utils.numFormat( - _memberController.userStat!['follower'], - ) - : '-', - style: - const TextStyle(fontWeight: FontWeight.bold)), - Text('粉丝', - style: TextStyle( - fontSize: Theme.of(context) - .textTheme - .labelMedium! - .fontSize)) - ], - ), - Column( - children: [ - const Text('-', - style: TextStyle(fontWeight: FontWeight.bold)), - Text( - '获赞', - style: TextStyle( - fontSize: Theme.of(context) - .textTheme - .labelMedium! - .fontSize), - ) - ], - ), - ], - ), - ), - const SizedBox(height: 10), - if (_memberController.ownerMid != _memberController.mid) ...[ - Row( - children: [ - TextButton( - onPressed: () {}, - style: TextButton.styleFrom( - padding: const EdgeInsets.only(left: 42, right: 42), - foregroundColor: - !loadingStatus && memberInfo.isFollowed - ? Theme.of(context).colorScheme.outline - : Theme.of(context).colorScheme.onPrimary, - backgroundColor: !loadingStatus && - memberInfo.isFollowed - ? Theme.of(context).colorScheme.onInverseSurface - : Theme.of(context) - .colorScheme - .primary, // 设置按钮背景色 - ), - child: Text(!loadingStatus && memberInfo.isFollowed - ? '取关' - : '关注'), - ), - const SizedBox(width: 8), - TextButton( - onPressed: () {}, - style: TextButton.styleFrom( - padding: const EdgeInsets.only(left: 42, right: 42), - backgroundColor: - Theme.of(context).colorScheme.onInverseSurface, - ), - child: const Text('发消息'), - ) - ], - ) - ] else ...[ - TextButton( - onPressed: () {}, - style: TextButton.styleFrom( - padding: const EdgeInsets.only(left: 80, right: 80), - foregroundColor: Theme.of(context).colorScheme.onPrimary, - backgroundColor: Theme.of(context).colorScheme.primary, - ), - child: const Text('编辑资料'), - ) - ] - ], - ), - ), - ], - ), - ); - } } diff --git a/lib/pages/member/widgets/profile.dart b/lib/pages/member/widgets/profile.dart new file mode 100644 index 00000000..c76cd39b --- /dev/null +++ b/lib/pages/member/widgets/profile.dart @@ -0,0 +1,193 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/models/live/item.dart'; +import 'package:pilipala/models/member/info.dart'; +import 'package:pilipala/utils/utils.dart'; + +Widget profile(ctr, {loadingStatus = false}) { + MemberInfoModel memberInfo = ctr.memberInfo.value; + return Builder( + builder: ((context) { + return Padding( + padding: EdgeInsets.only(top: 3 * MediaQuery.of(context).padding.top), + child: Row( + children: [ + Hero( + tag: ctr.heroTag!, + child: Stack( + children: [ + NetworkImgLayer( + width: 90, + height: 90, + type: 'avatar', + src: !loadingStatus ? memberInfo.face : ctr.face, + ), + if (!loadingStatus && + memberInfo.liveRoom != null && + memberInfo.liveRoom!.liveStatus == 1) + Positioned( + bottom: 0, + left: 14, + child: GestureDetector( + onTap: () { + LiveItemModel liveItem = LiveItemModel.fromJson({ + 'title': memberInfo.liveRoom!.title, + 'uname': memberInfo.name, + 'face': memberInfo.face, + 'roomid': memberInfo.liveRoom!.roomId, + 'watched_show': memberInfo.liveRoom!.watchedShow, + }); + Get.toNamed( + '/liveRoom?roomid=${memberInfo.liveRoom!.roomId}', + arguments: {'liveItem': liveItem}, + ); + }, + child: Container( + padding: const EdgeInsets.fromLTRB(6, 2, 6, 2), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: + const BorderRadius.all(Radius.circular(10)), + ), + child: Row(children: [ + Image.asset( + 'assets/images/live.gif', + height: 10, + ), + Text( + ' 直播中', + style: TextStyle( + color: Colors.white, + fontSize: Theme.of(context) + .textTheme + .labelSmall! + .fontSize), + ) + ]), + ), + ), + ) + ], + )), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Column( + children: [ + Text( + !loadingStatus + ? ctr.userStat!['following'].toString() + : '-', + style: + const TextStyle(fontWeight: FontWeight.bold), + ), + Text( + '关注', + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .labelMedium! + .fontSize), + ) + ], + ), + Column( + children: [ + Text( + !loadingStatus + ? Utils.numFormat( + ctr.userStat!['follower'], + ) + : '-', + style: const TextStyle( + fontWeight: FontWeight.bold)), + Text('粉丝', + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .labelMedium! + .fontSize)) + ], + ), + Column( + children: [ + const Text('-', + style: TextStyle(fontWeight: FontWeight.bold)), + Text( + '获赞', + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .labelMedium! + .fontSize), + ) + ], + ), + ], + ), + ), + const SizedBox(height: 10), + if (ctr.ownerMid != ctr.mid) ...[ + Row( + children: [ + TextButton( + onPressed: () => ctr.actionRelationMod(), + style: TextButton.styleFrom( + padding: const EdgeInsets.only(left: 42, right: 42), + foregroundColor: + !loadingStatus && memberInfo.isFollowed! + ? Theme.of(context).colorScheme.outline + : Theme.of(context).colorScheme.onPrimary, + backgroundColor: !loadingStatus && + memberInfo.isFollowed! + ? Theme.of(context).colorScheme.onInverseSurface + : Theme.of(context) + .colorScheme + .primary, // 设置按钮背景色 + ), + child: Text(!loadingStatus && memberInfo.isFollowed! + ? '取关' + : '关注'), + ), + const SizedBox(width: 8), + TextButton( + onPressed: () {}, + style: TextButton.styleFrom( + padding: const EdgeInsets.only(left: 42, right: 42), + backgroundColor: + Theme.of(context).colorScheme.onInverseSurface, + ), + child: const Text('发消息'), + ) + ], + ) + ] else ...[ + TextButton( + onPressed: () {}, + style: TextButton.styleFrom( + padding: const EdgeInsets.only(left: 80, right: 80), + foregroundColor: + Theme.of(context).colorScheme.onPrimary, + backgroundColor: Theme.of(context).colorScheme.primary, + ), + child: const Text('编辑资料'), + ) + ] + ], + ), + ), + ], + ), + ); + }), + ); +} diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index 4d0cba91..47e1f1c5 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -7,6 +7,7 @@ import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/http/constants.dart'; import 'package:pilipala/http/video.dart'; +import 'package:pilipala/models/common/reply_type.dart'; import 'package:pilipala/models/video/play/url.dart'; import 'package:pilipala/models/video/reply/item.dart'; import 'package:pilipala/pages/video/detail/replyReply/index.dart'; @@ -87,6 +88,8 @@ class VideoDetailController extends GetxController }, firstFloor: firstFloor, paddingTop: paddingTop, + replyType: ReplyType.video, + source: 'videoDetail', ); }); ctr?.closed.then((value) { @@ -95,6 +98,7 @@ class VideoDetailController extends GetxController } playerInit(source, audioSource, {Duration defaultST = Duration.zero}) { + meeduPlayerController.onVideoFitChange(BoxFit.cover); meeduPlayerController.setDataSource( DataSource( type: DataSourceType.network, @@ -157,7 +161,7 @@ class VideoDetailController extends GetxController @override void onClose() { markHeartBeat(); - if (timer!.isActive) { + if (timer != null && timer!.isActive) { timer!.cancel(); } super.onClose(); diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index db097dcc..6acd2e19 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -84,11 +84,11 @@ class VideoIntroController extends GetxController { Get.find(tag: Get.arguments['heroTag']) .tabs .value = ['简介', '评论 ${result['data']!.stat!.reply}']; + // 获取到粉丝数再返回 + await queryUserStat(); } else { responseMsg = result['msg']; } - // 获取到粉丝数再返回 - await queryUserStat(); if (userLogin) { // 获取点赞状态 queryHasLikeVideo(); @@ -99,13 +99,13 @@ class VideoIntroController extends GetxController { // queryFollowStatus(); } - return result; } // 获取up主粉丝数 Future queryUserStat() async { var result = await UserHttp.userStat(mid: videoDetail.value.owner!.mid!); + print('🌹:$result'); if (result['status']) { userStat = result['data']; } diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index 8d372942..2d500c14 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -65,7 +65,7 @@ class _VideoIntroPanelState extends State // 请求错误 return HttpError( errMsg: snapshot.data['msg'], - fn: () => setState(() {}), + fn: () => Get.back(), ); } } else { @@ -231,7 +231,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { return SliverPadding( - padding: const EdgeInsets.only(left: 12, right: 12, top: 20), + padding: const EdgeInsets.only(left: 12, right: 12, top: 10), sliver: SliverToBoxAdapter( child: !widget.loadingStatus || videoItem.isNotEmpty ? Column( @@ -248,6 +248,8 @@ class _VideoInfoState extends State with TickerProviderStateMixin { style: Theme.of(context).textTheme.titleMedium!.copyWith( letterSpacing: 0.5, ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ), InkWell( diff --git a/lib/pages/video/detail/player/controller.dart b/lib/pages/video/detail/player/controller.dart deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/pages/video/detail/player/index.dart b/lib/pages/video/detail/player/index.dart deleted file mode 100644 index c5d4ff78..00000000 --- a/lib/pages/video/detail/player/index.dart +++ /dev/null @@ -1,4 +0,0 @@ -library video_player; - -export './controller.dart'; -export './view.dart'; diff --git a/lib/pages/video/detail/player/view.dart b/lib/pages/video/detail/player/view.dart deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/pages/video/detail/reply/view.dart b/lib/pages/video/detail/reply/view.dart index c3a5271d..91484cce 100644 --- a/lib/pages/video/detail/reply/view.dart +++ b/lib/pages/video/detail/reply/view.dart @@ -6,6 +6,7 @@ import 'package:get/get.dart'; import 'package:pilipala/common/skeleton/video_reply.dart'; import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/models/video/reply/item.dart'; +import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/replyNew/index.dart'; import 'package:pilipala/utils/id_utils.dart'; import 'controller.dart'; @@ -52,9 +53,10 @@ class _VideoReplyPanelState extends State tag: widget.rpid.toString()); _videoReplyController.rPid = widget.rpid; } else { - int oid = Get.parameters['bvid'] != null - ? IdUtils.bv2av(Get.parameters['bvid']!) - : 0; + // fix 评论加载不对称 + // int oid = Get.parameters['bvid'] != null + // ? IdUtils.bv2av(Get.parameters['bvid']!) + // : 0; _videoReplyController = Get.put(VideoReplyController(oid, '', '1'), tag: Get.arguments['heroTag']); } @@ -113,6 +115,16 @@ class _VideoReplyPanelState extends State _videoReplyController.wakeUpReply(); } + // 展示二级回复 + void replyReply(replyItem, paddingTop) { + VideoDetailController videoDetailCtr = + Get.find(tag: Get.arguments['heroTag']); + videoDetailCtr.oid = replyItem.replies!.first.oid; + videoDetailCtr.fRpid = replyItem.rpid!; + videoDetailCtr.firstFloor = replyItem; + videoDetailCtr.showReplyReplyPanel(paddingTop); + } + @override void dispose() { super.dispose(); @@ -164,10 +176,13 @@ class _VideoReplyPanelState extends State ); } else { return ReplyItem( - replyItem: - _videoReplyController.replyList[index], - showReplyRow: true, - replyLevel: replyLevel); + replyItem: + _videoReplyController.replyList[index], + showReplyRow: true, + replyLevel: replyLevel, + replyReply: (replyItem, paddingTop) => + replyReply(replyItem, paddingTop), + ); } }, childCount: diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index 40335eba..ce6b9590 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -6,6 +6,7 @@ import 'package:flutter_meedu_media_kit/meedu_player.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/models/common/reply_type.dart'; import 'package:pilipala/models/video/reply/item.dart'; import 'package:pilipala/pages/video/detail/controller.dart'; import 'package:pilipala/pages/video/detail/reply/index.dart'; @@ -14,16 +15,21 @@ import 'package:pilipala/pages/video/detail/replyReply/index.dart'; import 'package:pilipala/utils/utils.dart'; class ReplyItem extends StatelessWidget { - ReplyItem( - {super.key, - this.replyItem, - this.addReply, - this.replyLevel, - this.showReplyRow}); + ReplyItem({ + super.key, + this.replyItem, + this.addReply, + this.replyLevel, + this.showReplyRow, + this.replyReply, + this.replyType, + }); ReplyItemModel? replyItem; Function? addReply; String? replyLevel; bool? showReplyRow = true; + Function? replyReply; + ReplyType? replyType; @override Widget build(BuildContext context) { @@ -212,6 +218,7 @@ class ReplyItem extends StatelessWidget { replyControl: replyItem!.replyControl, f_rpid: replyItem!.rpid, replyItem: replyItem, + replyReply: replyReply, ), ), ], @@ -272,11 +279,13 @@ class ReplyItem extends StatelessWidget { isScrollControlled: true, builder: (builder) { return VideoReplyNewDialog( - replyLevel: replyLevel, - oid: replyItem!.oid, - root: replyItem!.rpid, - parent: replyItem!.rpid, - paddingTop: paddingTop); + replyLevel: replyLevel, + oid: replyItem!.oid, + root: replyItem!.rpid, + parent: replyItem!.rpid, + paddingTop: paddingTop, + replyType: replyType, + ); }, ).then((value) => { // 完成评论,数据添加 @@ -320,21 +329,25 @@ class ReplyItem extends StatelessWidget { // ignore: must_be_immutable class ReplyItemRow extends StatelessWidget { - ReplyItemRow( - {super.key, - this.replies, - this.replyControl, - this.f_rpid, - this.replyItem}); + ReplyItemRow({ + super.key, + this.replies, + this.replyControl, + this.f_rpid, + this.replyItem, + this.replyReply, + }); List? replies; ReplyControl? replyControl; int? f_rpid; ReplyItemModel? replyItem; + Function? replyReply; @override Widget build(BuildContext context) { bool isShow = replyControl!.isShow!; int extraRow = replyControl != null && isShow ? 1 : 0; + double paddingTop = MediaQuery.of(context).padding.top; return Container( margin: const EdgeInsets.only(left: 42, right: 4, top: 0), child: Material( @@ -347,8 +360,7 @@ class ReplyItemRow extends StatelessWidget { children: [ for (var i = 0; i < replies!.length; i++) ...[ InkWell( - onTap: () => - replyReply(replyItem, MediaQuery.of(context).padding.top), + onTap: () => replyReply!(replyItem, paddingTop), child: Container( width: double.infinity, padding: EdgeInsets.fromLTRB( @@ -398,8 +410,7 @@ class ReplyItemRow extends StatelessWidget { ], if (extraRow == 1) InkWell( - onTap: () => - replyReply(replyItem, MediaQuery.of(context).padding.top), + onTap: () => replyReply!(replyItem, paddingTop), child: Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(8, 5, 8, 8), @@ -428,16 +439,6 @@ class ReplyItemRow extends StatelessWidget { ), ); } - - void replyReply(replyItem, paddingTop) { - // replyItem 楼主评论 - VideoDetailController videoDetailCtr = - Get.find(tag: Get.arguments['heroTag']); - videoDetailCtr.oid = replies!.first.oid; - videoDetailCtr.fRpid = f_rpid!; - videoDetailCtr.firstFloor = replyItem; - videoDetailCtr.showReplyReplyPanel(paddingTop); - } } InlineSpan buildContent(BuildContext context, content) { diff --git a/lib/pages/video/detail/replyNew/view.dart b/lib/pages/video/detail/replyNew/view.dart index 53b910ff..4d1a52c9 100644 --- a/lib/pages/video/detail/replyNew/view.dart +++ b/lib/pages/video/detail/replyNew/view.dart @@ -13,6 +13,7 @@ class VideoReplyNewDialog extends StatefulWidget { String? replyLevel; int? parent; double? paddingTop; + ReplyType? replyType; VideoReplyNewDialog({ this.oid, @@ -20,6 +21,7 @@ class VideoReplyNewDialog extends StatefulWidget { this.replyLevel, this.parent, this.paddingTop, + this.replyType, }); @override @@ -64,7 +66,7 @@ class _VideoReplyNewDialogState extends State Future submitReplyAdd() async { String message = _replyContentController.text; var result = await VideoHttp.replyAdd( - type: ReplyType.video, + type: widget.replyType!, oid: widget.oid!, root: widget.root!, parent: widget.parent!, diff --git a/lib/pages/video/detail/replyReply/controller.dart b/lib/pages/video/detail/replyReply/controller.dart index 26653d41..9f7c7a94 100644 --- a/lib/pages/video/detail/replyReply/controller.dart +++ b/lib/pages/video/detail/replyReply/controller.dart @@ -1,16 +1,18 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/http/reply.dart'; +import 'package:pilipala/models/common/reply_type.dart'; import 'package:pilipala/models/video/reply/data.dart'; import 'package:pilipala/models/video/reply/item.dart'; class VideoReplyReplyController extends GetxController { - VideoReplyReplyController(this.aid, this.rpid); + VideoReplyReplyController(this.aid, this.rpid, this.replyType); final ScrollController scrollController = ScrollController(); // 视频aid 请求时使用的oid int? aid; // rpid 请求楼中楼回复 String? rpid; + ReplyType replyType = ReplyType.video; RxList replyList = [ReplyItemModel()].obs; // 当前页 int currentPage = 0; @@ -41,7 +43,10 @@ class VideoReplyReplyController extends GetxController { } isLoadingMore = true; var res = await ReplyHttp.replyReplyList( - oid: aid!, root: rpid!, pageNum: currentPage + 1, type: 1); + oid: aid!, + root: rpid!, + pageNum: currentPage + 1, + type: replyType.index); if (res['status']) { res['data'] = ReplyData.fromJson(res['data']); if (res['data'].replies.isNotEmpty) { diff --git a/lib/pages/video/detail/replyReply/view.dart b/lib/pages/video/detail/replyReply/view.dart index 49662879..bfda7f76 100644 --- a/lib/pages/video/detail/replyReply/view.dart +++ b/lib/pages/video/detail/replyReply/view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/skeleton/video_reply.dart'; import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/models/common/reply_type.dart'; import 'package:pilipala/models/video/reply/item.dart'; import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart'; @@ -13,6 +14,8 @@ class VideoReplyReplyPanel extends StatefulWidget { Function? closePanel; ReplyItemModel? firstFloor; double? paddingTop; + String? source; + ReplyType? replyType; VideoReplyReplyPanel({ this.oid, @@ -20,6 +23,8 @@ class VideoReplyReplyPanel extends StatefulWidget { this.closePanel, this.firstFloor, this.paddingTop, + this.source, + this.replyType, super.key, }); @@ -34,7 +39,8 @@ class _VideoReplyReplyPanelState extends State { @override void initState() { _videoReplyReplyController = Get.put( - VideoReplyReplyController(widget.oid, widget.rpid.toString()), + VideoReplyReplyController( + widget.oid, widget.rpid.toString(), widget.replyType!), tag: widget.rpid.toString()); super.initState(); @@ -62,34 +68,37 @@ class _VideoReplyReplyPanelState extends State { @override Widget build(BuildContext context) { return Container( - height: MediaQuery.of(context).size.height - - MediaQuery.of(context).size.width * 9 / 16 - - widget.paddingTop!, + height: widget.source == 'videoDetail' + ? MediaQuery.of(context).size.height - + MediaQuery.of(context).size.width * 9 / 16 - + widget.paddingTop! + : null, color: Theme.of(context).colorScheme.background, child: Column( children: [ - Container( - height: 45, - padding: const EdgeInsets.only(left: 14, right: 14), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '评论详情', - style: Theme.of(context).textTheme.titleMedium, - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () { - _videoReplyReplyController.currentPage = 0; - _videoReplyReplyController.rPid = 0; - widget.closePanel!(); - Navigator.pop(context); - }, - ), - ], + if (widget.source == 'videoDetail') + Container( + height: 45, + padding: const EdgeInsets.only(left: 14, right: 14), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '评论详情', + style: Theme.of(context).textTheme.titleMedium, + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + _videoReplyReplyController.currentPage = 0; + _videoReplyReplyController.rPid = 0; + widget.closePanel!(); + Navigator.pop(context); + }, + ), + ], + ), ), - ), Divider( height: 1, color: Theme.of(context).dividerColor.withOpacity(0.1), @@ -188,7 +197,7 @@ class _VideoReplyReplyPanelState extends State { delegate: SliverChildBuilderDelegate((context, index) { return const VideoReplySkeleton(); - }, childCount: 5), + }, childCount: 8), ); } }, diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index fcd3c021..ec204b3a 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -90,14 +90,16 @@ class _VideoDetailPageState extends State @override void dispose() { videoDetailController.meeduPlayerController.dispose(); - videoDetailController.timer!.cancel(); + if (videoDetailController.timer != null) { + videoDetailController.timer!.cancel(); + } super.dispose(); } @override // 离开当前页面时 void didPushNext() async { - if (!_meeduPlayerController!.pipEnabled) { + if (!_meeduPlayerController!.pipAvailable.value) { _meeduPlayerController!.pause(); } if (videoDetailController.timer!.isActive) { @@ -232,7 +234,7 @@ class _VideoDetailPageState extends State children: [ Container( width: double.infinity, - height: 0, + height: 45, decoration: BoxDecoration( border: Border( bottom: BorderSide( @@ -252,8 +254,8 @@ class _VideoDetailPageState extends State () => TabBar( controller: videoDetailController.tabCtr, dividerColor: Colors.transparent, - indicatorColor: - Theme.of(context).colorScheme.background, + // indicatorColor: + // Theme.of(context).colorScheme.background, tabs: videoDetailController.tabs .map((String name) => Tab(text: name)) .toList(), @@ -278,7 +280,9 @@ class _VideoDetailPageState extends State ); }, ), - VideoReplyPanel() + VideoReplyPanel( + bvid: videoDetailController.bvid, + ) ], ), ), diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 5474607c..3d0f4fb9 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -15,6 +15,7 @@ import 'package:pilipala/pages/preview/index.dart'; import 'package:pilipala/pages/search/index.dart'; import 'package:pilipala/pages/searchResult/index.dart'; import 'package:pilipala/pages/video/detail/index.dart'; +import 'package:pilipala/pages/video/detail/replyReply/index.dart'; import 'package:pilipala/pages/webview/index.dart'; import 'package:pilipala/pages/setting/index.dart'; import 'package:pilipala/pages/media/index.dart'; @@ -65,5 +66,7 @@ class Routes { GetPage(name: '/liveRoom', page: () => const LiveRoomPage()), // 用户中心 GetPage(name: '/member', page: () => const MemberPage()), + // 二级回复 + GetPage(name: '/replyReply', page: () => VideoReplyReplyPanel()), ]; } diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 0bbd4136..df3c92b8 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -12,6 +12,7 @@ class GStrorage { static late final Box userInfo; static late final Box hotKeyword; static late final Box historyword; + static late final Box localCache; static Future init() async { final dir = await getApplicationDocumentsDirectory(); @@ -28,6 +29,8 @@ class GStrorage { hotKeyword = await Hive.openBox('hotKeyword'); // 搜索历史 historyword = await Hive.openBox('historyWord'); + // 本地缓存 + localCache = await Hive.openBox('localCache'); } static regAdapter() { diff --git a/lib/utils/wbi_sign.dart b/lib/utils/wbi_sign.dart index 419884ce..39c88389 100644 --- a/lib/utils/wbi_sign.dart +++ b/lib/utils/wbi_sign.dart @@ -2,11 +2,15 @@ // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/sign/wbi.md // import md5 from 'md5' // import axios from 'axios' +import 'package:hive/hive.dart'; import 'package:pilipala/http/index.dart'; import 'package:crypto/crypto.dart'; import 'dart:convert'; +import 'package:pilipala/utils/storage.dart'; + class WbiSign { + static Box localCache = GStrorage.user; List mixinKeyEncTab = [ 46, 47, @@ -83,7 +87,7 @@ class WbiSign { } // 为请求参数进行 wbi 签名 - String encWbi(params, imgKey, subKey) { + Map encWbi(params, imgKey, subKey) { String mixinKey = getMixinKey(imgKey + subKey); DateTime now = DateTime.now(); int currTime = (now.millisecondsSinceEpoch / 1000).round(); @@ -99,19 +103,25 @@ class WbiSign { String queryStr = query.join('&'); String wbiSign = md5.convert(utf8.encode(queryStr + mixinKey)).toString(); // 计算 w_rid - print('w_rid: $wbiSign'); - return '$queryStr&w_rid=$wbiSign'; + return {'wts': currTime.toString(), 'w_rid': wbiSign}; } - // 获取最新的 img_key 和 sub_key + // 获取最新的 img_key 和 sub_key 可以从缓存中获取 static Future> getWbiKeys() async { + DateTime nowDate = DateTime.now(); + if (localCache.get('wbiKeys') != null && + DateTime.fromMillisecondsSinceEpoch(localCache.get('timeStamp')).day == + nowDate.day) { + Map cacheWbiKeys = localCache.get('wbiKeys'); + return Map.from(cacheWbiKeys); + } var resp = await Request().get('https://api.bilibili.com/x/web-interface/nav'); var jsonContent = resp.data['data']; String imgUrl = jsonContent['wbi_img']['img_url']; String subUrl = jsonContent['wbi_img']['sub_url']; - return { + Map wbiKeys = { 'imgKey': imgUrl .substring(imgUrl.lastIndexOf('/') + 1, imgUrl.length) .split('.')[0], @@ -119,12 +129,16 @@ class WbiSign { .substring(subUrl.lastIndexOf('/') + 1, subUrl.length) .split('.')[0] }; + localCache.put('wbiKeys', wbiKeys); + localCache.put('timeStamp', nowDate.millisecondsSinceEpoch); + return wbiKeys; } makSign(Map params) async { // params 为需要加密的请求参数 Map wbiKeys = await getWbiKeys(); - String query = encWbi(params, wbiKeys['imgKey'], wbiKeys['subKey']); + Map query = params + ..addAll(encWbi(params, wbiKeys['imgKey'], wbiKeys['subKey'])); return query; } } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c4791877..bc7c9f97 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -17,7 +17,6 @@ import screen_retriever import share_plus import shared_preferences_foundation import sqflite -import url_launcher_macos import wakelock_macos import window_manager @@ -34,7 +33,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) - UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index dc53572e..6908f366 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -213,10 +213,10 @@ packages: dependency: "direct main" description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" cupertino_icons: dependency: "direct main" description: @@ -301,10 +301,10 @@ packages: dependency: "direct main" description: name: dynamic_color - sha256: bbebb1b7ebed819e0ec83d4abdc2a8482d934f6a85289ffc1c6acf7589fa2aad + sha256: de4798a7069121aee12d5895315680258415de9b00e717723a1bd73d58f0126d url: "https://pub.dev" source: hosted - version: "1.6.3" + version: "1.6.6" extended_image: dependency: "direct main" description: @@ -424,7 +424,7 @@ packages: description: path: package ref: feature-custom - resolved-ref: ea73de29401ab35126ef3eac68270b2e6c3ef4e0 + resolved-ref: d2bc690f3bf601feaa06085479abc384f0dc5168 url: "https://github.com/guozhigq/flutter_meedu_media_kit.git" source: git version: "4.2.12" @@ -432,10 +432,10 @@ packages: dependency: "direct main" description: name: flutter_smart_dialog - sha256: da7ed8fe78e301e3c2cdaa533d13ed3edcf1853c1ba1a7383b481739569f7b2a + sha256: "8ba9eeb5b0b380bec368c5c8a324e1dab0cd88965c2dd83e64237441140bc599" url: "https://pub.dev" source: hosted - version: "4.9.0+6" + version: "4.9.3+2" flutter_spinkit: dependency: transitive description: @@ -926,6 +926,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.3" + pull_to_refresh_notification: + dependency: "direct main" + description: + name: pull_to_refresh_notification + sha256: "3f27b9695c98770db3f9f50550e5ab44a6d946d022311a55bbe6d5cd4c69a1ad" + url: "https://pub.dev" + source: hosted + version: "3.0.1" rxdart: dependency: transitive description: @@ -1219,30 +1227,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" - url_launcher: - dependency: "direct main" - description: - name: url_launcher - sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e" - url: "https://pub.dev" - source: hosted - version: "6.1.10" - url_launcher_android: - dependency: transitive - description: - name: url_launcher_android - sha256: "22f8db4a72be26e9e3a4aa3f194b1f7afbc76d20ec141f84be1d787db2155cbd" - url: "https://pub.dev" - source: hosted - version: "6.0.31" - url_launcher_ios: - dependency: transitive - description: - name: url_launcher_ios - sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" - url: "https://pub.dev" - source: hosted - version: "6.1.4" url_launcher_linux: dependency: transitive description: @@ -1251,14 +1235,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.5" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" - url: "https://pub.dev" - source: hosted - version: "3.0.5" url_launcher_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 17d711cd..83827a54 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,9 +34,9 @@ dependencies: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 + cupertino_icons: ^1.0.5 # 动态取色 - dynamic_color: ^1.6.2 + dynamic_color: ^1.6.6 get: ^4.6.5 @@ -63,18 +63,20 @@ dependencies: permission_handler: ^10.2.0 # 分享 share_plus: ^6.3.1 - # webView - url_launcher: ^6.1.9 # cookie 管理 webview_cookie_manager: ^2.0.6 # 浏览器 webview_flutter: ^4.2.0 # 解决sliver滑动不同步 extended_nested_scroll_view: ^6.0.0 + # 上拉加载 + loading_more_list: ^5.0.3 + # 下拉刷新 + pull_to_refresh_notification: ^3.0.1 # 图标 font_awesome_flutter: ^10.4.0 # toast - flutter_smart_dialog: ^4.9.0+6 + flutter_smart_dialog: ^4.9.3+2 # 下滑关闭 dismissible_page: ^1.0.2 # 媒体播放 @@ -85,8 +87,8 @@ dependencies: ref: feature-custom path: package custom_sliding_segmented_control: ^1.7.5 - loading_more_list: ^5.0.3 - crypto: any + # 加密 + crypto: ^3.0.3 dev_dependencies: flutter_test: