From 5e63f0da7b93eb3e2d2b27ed0ba8b52d24518f50 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sat, 15 Jun 2024 16:03:45 +0800 Subject: [PATCH 01/19] =?UTF-8?q?feat:=20=E7=A7=81=E4=BF=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/http/msg.dart | 66 ++-- lib/pages/member/widgets/profile.dart | 12 +- lib/pages/whisper/controller.dart | 9 + lib/pages/whisper/view.dart | 23 +- lib/pages/whisper_detail/controller.dart | 37 +- lib/pages/whisper_detail/view.dart | 342 ++++++++++-------- .../whisper_detail/widget/chat_item.dart | 162 +++++---- 7 files changed, 373 insertions(+), 278 deletions(-) diff --git a/lib/http/msg.dart b/lib/http/msg.dart index d1d31958..7ac30bc1 100644 --- a/lib/http/msg.dart +++ b/lib/http/msg.dart @@ -1,4 +1,6 @@ +import 'dart:convert'; import 'dart:math'; +import 'package:dio/dio.dart'; import '../models/msg/account.dart'; import '../models/msg/session.dart'; import '../utils/wbi_sign.dart'; @@ -134,56 +136,42 @@ class MsgHttp { // 发送私信 static Future sendMsg({ - int? senderUid, - int? receiverId, + required int senderUid, + required int receiverId, int? receiverType, int? msgType, dynamic content, }) async { String csrf = await Request.getCsrf(); - Map params = await WbiSign().makSign({ - 'msg[sender_uid]': senderUid, - 'msg[receiver_id]': receiverId, - 'msg[receiver_type]': receiverType ?? 1, - 'msg[msg_type]': msgType ?? 1, - 'msg[msg_status]': 0, - 'msg[dev_id]': getDevId(), - 'msg[timestamp]': DateTime.now().millisecondsSinceEpoch ~/ 1000, - 'msg[new_face_version]': 0, - 'msg[content]': content, - 'from_firework': 0, - 'build': 0, - 'mobi_app': 'web', - 'csrf_token': csrf, - 'csrf': csrf, - }); - var res = - await Request().post(Api.sendMsg, queryParameters: { - ...params, - 'csrf_token': csrf, - 'csrf': csrf, - }, data: { - 'w_sender_uid': params['msg[sender_uid]'], - 'w_receiver_id': params['msg[receiver_id]'], - 'w_dev_id': params['msg[dev_id]'], - 'w_rid': params['w_rid'], - 'wts': params['wts'], - 'csrf_token': csrf, - 'csrf': csrf, - }); + var res = await Request().post( + Api.sendMsg, + data: { + 'msg[sender_uid]': senderUid, + 'msg[receiver_id]': receiverId, + 'msg[receiver_type]': 1, + 'msg[msg_type]': 1, + 'msg[msg_status]': 0, + 'msg[content]': jsonEncode(content), + 'msg[timestamp]': DateTime.now().millisecondsSinceEpoch ~/ 1000, + 'msg[new_face_version]': 0, + 'msg[dev_id]': getDevId(), + 'from_firework': 0, + 'build': 0, + 'mobi_app': 'web', + 'csrf_token': csrf, + 'csrf': csrf, + }, + options: Options( + contentType: Headers.formUrlEncodedContentType, + ), + ); if (res.data['code'] == 0) { return { 'status': true, 'data': res.data['data'], }; } else { - return { - 'status': false, - 'date': [], - 'msg': "message: ${res.data['message']}," - " msg: ${res.data['msg']}," - " code: ${res.data['code']}", - }; + return {'status': false, 'date': [], 'msg': res.data['message']}; } } diff --git a/lib/pages/member/widgets/profile.dart b/lib/pages/member/widgets/profile.dart index a708a35e..8c6385db 100644 --- a/lib/pages/member/widgets/profile.dart +++ b/lib/pages/member/widgets/profile.dart @@ -208,7 +208,17 @@ class ProfilePanel extends StatelessWidget { const SizedBox(width: 8), Expanded( child: TextButton( - onPressed: () {}, + onPressed: () { + Get.toNamed( + '/whisperDetail', + parameters: { + 'name': memberInfo.name!, + 'face': memberInfo.face!, + 'mid': memberInfo.mid.toString(), + 'heroTag': ctr.heroTag!, + }, + ); + }, style: TextButton.styleFrom( backgroundColor: Theme.of(context) .colorScheme diff --git a/lib/pages/whisper/controller.dart b/lib/pages/whisper/controller.dart index c82cab5b..b7c52d2e 100644 --- a/lib/pages/whisper/controller.dart +++ b/lib/pages/whisper/controller.dart @@ -62,4 +62,13 @@ class WhisperController extends GetxController { Future onRefresh() async { querySessionList('onRefresh'); } + + void refreshLastMsg(int talkerId, String content) { + final SessionList currentItem = + sessionList.where((p0) => p0.talkerId == talkerId).first; + currentItem.lastMsg!.content['content'] = content; + sessionList.removeWhere((p0) => p0.talkerId == talkerId); + sessionList.insert(0, currentItem); + sessionList.refresh(); + } } diff --git a/lib/pages/whisper/view.dart b/lib/pages/whisper/view.dart index fa95463b..6d2f281b 100644 --- a/lib/pages/whisper/view.dart +++ b/lib/pages/whisper/view.dart @@ -202,6 +202,7 @@ class SessionItem extends StatelessWidget { @override Widget build(BuildContext context) { + final String heroTag = Utils.makeHeroTag(sessionItem.accountInfo.mid); final content = sessionItem.lastMsg.content; return ListTile( onTap: () { @@ -214,6 +215,7 @@ class SessionItem extends StatelessWidget { 'name': sessionItem.accountInfo.name, 'face': sessionItem.accountInfo.face, 'mid': sessionItem.accountInfo.mid.toString(), + 'heroTag': heroTag, }, ); }, @@ -221,11 +223,14 @@ class SessionItem extends StatelessWidget { isLabelVisible: sessionItem.unreadCount > 0, label: Text(sessionItem.unreadCount.toString()), alignment: Alignment.topRight, - child: NetworkImgLayer( - width: 45, - height: 45, - type: 'avatar', - src: sessionItem.accountInfo.face, + child: Hero( + tag: heroTag, + child: NetworkImgLayer( + width: 45, + height: 45, + type: 'avatar', + src: sessionItem.accountInfo.face, + ), ), ), title: Text(sessionItem.accountInfo.name), @@ -245,10 +250,10 @@ class SessionItem extends StatelessWidget { .copyWith(color: Theme.of(context).colorScheme.outline)), trailing: Text( Utils.dateFormat(sessionItem.lastMsg.timestamp), - style: Theme.of(context) - .textTheme - .labelSmall! - .copyWith(color: Theme.of(context).colorScheme.outline), + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelSmall!.fontSize, + color: Theme.of(context).colorScheme.outline, + ), ), ); } diff --git a/lib/pages/whisper_detail/controller.dart b/lib/pages/whisper_detail/controller.dart index 6e950f81..3c7e0837 100644 --- a/lib/pages/whisper_detail/controller.dart +++ b/lib/pages/whisper_detail/controller.dart @@ -1,30 +1,39 @@ +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/http/msg.dart'; import 'package:pilipala/models/msg/session.dart'; +import 'package:pilipala/pages/whisper/index.dart'; import '../../utils/feed_back.dart'; import '../../utils/storage.dart'; class WhisperDetailController extends GetxController { - late int talkerId; + int? talkerId; late String name; late String face; late String mid; + late String heroTag; RxList messageList = [].obs; //表情转换图片规则 - List? eInfos; + RxList eInfos = [].obs; final TextEditingController replyContentController = TextEditingController(); Box userInfoCache = GStrorage.userInfo; + List emoteList = []; @override void onInit() { super.onInit(); - talkerId = int.parse(Get.parameters['talkerId']!); + if (Get.parameters.containsKey('talkerId')) { + talkerId = int.parse(Get.parameters['talkerId']!); + } else { + talkerId = int.parse(Get.parameters['mid']!); + } name = Get.parameters['name']!; face = Get.parameters['face']!; mid = Get.parameters['mid']!; + heroTag = Get.parameters['heroTag']!; } Future querySessionMsg() async { @@ -34,7 +43,7 @@ class WhisperDetailController extends GetxController { if (messageList.isNotEmpty) { ackSessionMsg(); if (res['data'].eInfos != null) { - eInfos = res['data'].eInfos; + eInfos.value = res['data'].eInfos; } } } else { @@ -73,7 +82,25 @@ class WhisperDetailController extends GetxController { msgType: 1, ); if (result['status']) { - SmartDialog.showToast('发送成功'); + String content = jsonDecode(result['data']['msg_content'])['content']; + messageList.insert( + 0, + MessageItem( + msgSeqno: result['data']['msg_key'], + senderUid: userInfo.mid, + receiverId: int.parse(mid), + content: {'content': content}, + msgType: 1, + timestamp: DateTime.now().millisecondsSinceEpoch, + ), + ); + eInfos.addAll(emoteList); + replyContentController.clear(); + try { + late final WhisperController whisperController = + Get.find(); + whisperController.refreshLastMsg(talkerId!, message); + } catch (_) {} } else { SmartDialog.showToast(result['msg']); } diff --git a/lib/pages/whisper_detail/view.dart b/lib/pages/whisper_detail/view.dart index 1701be33..7c5762d9 100644 --- a/lib/pages/whisper_detail/view.dart +++ b/lib/pages/whisper_detail/view.dart @@ -1,9 +1,12 @@ import 'dart:async'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/models/video/reply/emote.dart'; import 'package:pilipala/pages/emote/index.dart'; +import 'package:pilipala/pages/video/detail/reply_new/toolbar_icon_button.dart'; import 'package:pilipala/pages/whisper_detail/controller.dart'; import 'package:pilipala/utils/feed_back.dart'; import '../../utils/storage.dart'; @@ -24,9 +27,9 @@ class _WhisperDetailPageState extends State late TextEditingController _replyContentController; final FocusNode replyContentFocusNode = FocusNode(); final _debouncer = Debouncer(milliseconds: 200); // 设置延迟时间 - late double emoteHeight = 0.0; + late double emoteHeight = 230.0; double keyboardHeight = 0.0; // 键盘高度 - String toolbarType = 'input'; + RxString toolbarType = ''.obs; Box userInfoCache = GStrorage.userInfo; @override @@ -41,9 +44,7 @@ class _WhisperDetailPageState extends State _focuslistener() { replyContentFocusNode.addListener(() { if (replyContentFocusNode.hasFocus) { - setState(() { - toolbarType = 'input'; - }); + toolbarType.value = 'input'; } }); } @@ -52,7 +53,7 @@ class _WhisperDetailPageState extends State void didChangeMetrics() { super.didChangeMetrics(); final String routePath = Get.currentRoute; - if (mounted && routePath.startsWith('/whisper_detail')) { + if (mounted && routePath.startsWith('/whisperDetail')) { WidgetsBinding.instance.addPostFrameCallback((_) { // 键盘高度 final viewInsets = EdgeInsets.fromViewPadding( @@ -61,8 +62,11 @@ class _WhisperDetailPageState extends State if (mounted) { if (keyboardHeight == 0) { setState(() { - emoteHeight = keyboardHeight = + keyboardHeight = keyboardHeight == 0.0 ? viewInsets.bottom : keyboardHeight; + if (keyboardHeight != 0) { + emoteHeight = keyboardHeight; + } }); } } @@ -79,6 +83,23 @@ class _WhisperDetailPageState extends State super.dispose(); } + void onChooseEmote(PackageItem package, Emote emote) { + _whisperDetailController.emoteList.add( + {'text': emote.text, 'url': emote.url}, + ); + final int cursorPosition = + max(_replyContentController.selection.baseOffset, 0); + final String currentText = _replyContentController.text; + final String newText = currentText.substring(0, cursorPosition) + + emote.text! + + currentText.substring(cursorPosition); + _replyContentController.value = TextEditingValue( + text: newText, + selection: + TextSelection.collapsed(offset: cursorPosition + emote.text!.length), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -88,30 +109,20 @@ class _WhisperDetailPageState extends State width: double.infinity, height: 50, child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SizedBox( width: 34, height: 34, child: IconButton( - style: ButtonStyle( - padding: MaterialStateProperty.all(EdgeInsets.zero), - backgroundColor: MaterialStateProperty.resolveWith( - (Set states) { - return Theme.of(context) - .colorScheme - .primaryContainer - .withOpacity(0.6); - }), - ), onPressed: () => Get.back(), icon: Icon( - Icons.arrow_back_outlined, + Icons.arrow_back_ios, size: 18, color: Theme.of(context).colorScheme.onPrimaryContainer, ), ), ), + const SizedBox(width: 10), GestureDetector( onTap: () { feedBack(); @@ -125,11 +136,14 @@ class _WhisperDetailPageState extends State }, child: Row( children: [ - NetworkImgLayer( - width: 34, - height: 34, - type: 'avatar', - src: _whisperDetailController.face, + Hero( + tag: _whisperDetailController.heroTag, + child: NetworkImgLayer( + width: 34, + height: 34, + type: 'avatar', + src: _whisperDetailController.face, + ), ), const SizedBox(width: 6), Text( @@ -143,155 +157,169 @@ class _WhisperDetailPageState extends State ], ), ), - ), - body: GestureDetector( - onTap: () { - FocusScope.of(context).unfocus(); - setState(() { - keyboardHeight = 0; - }); - }, - child: FutureBuilder( - future: _futureBuilderFuture, - builder: (BuildContext context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - if (snapshot.data == null) { - return const SizedBox(); - } - final Map data = snapshot.data as Map; - if (data['status']) { - List messageList = _whisperDetailController.messageList; - return Obx( - () => messageList.isEmpty - ? const SizedBox() - : ListView.builder( - itemCount: messageList.length, - shrinkWrap: true, - reverse: true, - itemBuilder: (_, int i) { - if (i == 0) { - return Column( - children: [ - ChatItem( - item: messageList[i], - e_infos: _whisperDetailController.eInfos), - const SizedBox(height: 12), - ], - ); - } else { - return ChatItem( - item: messageList[i], - e_infos: _whisperDetailController.eInfos); - } - }, - ), - ); - } else { - // 请求错误 - return const SizedBox(); - } - } else { - // 骨架屏 - return const SizedBox(); - } - }, - ), - ), - // resizeToAvoidBottomInset: true, - bottomNavigationBar: Container( - width: double.infinity, - height: MediaQuery.of(context).padding.bottom + 70 + keyboardHeight, - padding: EdgeInsets.only( - left: 8, - right: 12, - top: 12, - bottom: MediaQuery.of(context).padding.bottom, - ), - decoration: BoxDecoration( - border: Border( - top: BorderSide( - width: 4, - color: Theme.of(context).colorScheme.primary.withOpacity(0.1), + actions: [ + IconButton( + onPressed: () {}, + icon: const Icon( + Icons.more_vert_outlined, + size: 20, ), ), - ), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // IconButton( - // onPressed: () {}, - // icon: Icon( - // Icons.add_circle_outline, - // color: Theme.of(context).colorScheme.outline, - // ), - // ), - IconButton( - onPressed: () { - // if (toolbarType == 'input') { - // setState(() { - // toolbarType = 'emote'; - // }); - // } - // FocusScope.of(context).unfocus(); - }, - icon: Icon( - Icons.emoji_emotions_outlined, - color: Theme.of(context).colorScheme.outline, + const SizedBox(width: 14) + ], + ), + body: Column( + children: [ + Expanded( + child: GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); + toolbarType.value = ''; + }, + child: FutureBuilder( + future: _futureBuilderFuture, + builder: (BuildContext context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.data == null) { + return const SizedBox(); + } + final Map data = snapshot.data as Map; + if (data['status']) { + List messageList = _whisperDetailController.messageList; + return Obx( + () => messageList.isEmpty + ? const SizedBox() + : Align( + alignment: Alignment.topCenter, + child: ListView.builder( + itemCount: messageList.length, + shrinkWrap: true, + reverse: true, + itemBuilder: (_, int i) { + if (i == 0) { + return Column( + children: [ + ChatItem( + item: messageList[i], + e_infos: _whisperDetailController + .eInfos), + const SizedBox(height: 20), + ], + ); + } else { + return ChatItem( + item: messageList[i], + e_infos: + _whisperDetailController.eInfos); + } + }, + ), + ), + ); + } else { + // 请求错误 + return const SizedBox(); + } + } else { + // 骨架屏 + return const SizedBox(); + } + }, + ), + ), + ), + Obx( + () => Container( + padding: EdgeInsets.fromLTRB( + 8, + 12, + 12, + toolbarType.value == '' + ? MediaQuery.of(context).padding.bottom + 6 + : 6, + ), + decoration: BoxDecoration( + border: Border( + top: BorderSide( + width: 1, + color: Colors.grey.withOpacity(0.15), ), ), - Expanded( - child: Container( - height: 45, - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .primary - .withOpacity(0.08), - borderRadius: BorderRadius.circular(40.0), - ), - child: TextField( - readOnly: true, - style: Theme.of(context).textTheme.titleMedium, - controller: _replyContentController, - autofocus: false, - focusNode: replyContentFocusNode, - decoration: const InputDecoration( - border: InputBorder.none, // 移除默认边框 - hintText: '开发中 ...', // 提示文本 - contentPadding: EdgeInsets.symmetric( - horizontal: 16.0, vertical: 12.0), // 内边距 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ToolbarIconButton( + onPressed: () { + if (toolbarType.value == '') { + toolbarType.value = 'emote'; + } else if (toolbarType.value == 'input') { + FocusScope.of(context).unfocus(); + toolbarType.value = 'emote'; + } else if (toolbarType.value == 'emote') { + FocusScope.of(context).requestFocus(); + } + }, + icon: const Icon(Icons.emoji_emotions_outlined, size: 22), + toolbarType: toolbarType.value, + selected: false, + ), + const SizedBox(width: 4), + Expanded( + child: Container( + height: 45, + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .outline + .withOpacity(0.05), + borderRadius: BorderRadius.circular(40.0), + ), + child: TextField( + style: Theme.of(context).textTheme.titleMedium, + controller: _replyContentController, + autofocus: false, + focusNode: replyContentFocusNode, + decoration: const InputDecoration( + border: InputBorder.none, // 移除默认边框 + hintText: '文明发言 ~', // 提示文本 + contentPadding: EdgeInsets.symmetric( + horizontal: 16.0, vertical: 12.0), // 内边距 + ), ), ), ), - ), - IconButton( - // onPressed: _whisperDetailController.sendMsg, - onPressed: null, - icon: Icon( - Icons.send, - color: Theme.of(context).colorScheme.outline, + IconButton( + onPressed: _whisperDetailController.sendMsg, + icon: Icon( + Icons.send, + color: Theme.of(context).colorScheme.outline, + ), ), - ), - // const SizedBox(width: 16), - ], + ], + ), ), - AnimatedSize( - curve: Curves.easeInOut, - duration: const Duration(milliseconds: 300), + ), + Obx( + () => AnimatedSize( + curve: Curves.linear, + duration: const Duration(milliseconds: 200), child: SizedBox( width: double.infinity, - height: toolbarType == 'input' ? keyboardHeight : emoteHeight, + height: toolbarType.value == 'input' + ? keyboardHeight + : toolbarType.value == 'emote' + ? emoteHeight + : 0, child: EmotePanel( - onChoose: (package, emote) => {}, + onChoose: (package, emote) => onChooseEmote(package, emote), ), ), ), - ], - ), + ), + ], ), + resizeToAvoidBottomInset: false, ); } } diff --git a/lib/pages/whisper_detail/widget/chat_item.dart b/lib/pages/whisper_detail/widget/chat_item.dart index 0d37e8b3..c0d87221 100644 --- a/lib/pages/whisper_detail/widget/chat_item.dart +++ b/lib/pages/whisper_detail/widget/chat_item.dart @@ -1,7 +1,6 @@ // ignore_for_file: must_be_immutable import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -69,9 +68,13 @@ class ChatItem extends StatelessWidget { Color textColor(BuildContext context) { return isOwner ? Theme.of(context).colorScheme.onPrimary - : Theme.of(context).colorScheme.onSecondaryContainer; + : Theme.of(context).colorScheme.onBackground; } + const double safeDistanceval = 6; + const double borderRadiusVal = 12; + const double paddingVal = 10; + Widget richTextMessage(BuildContext context) { var text = content['content']; if (e_infos != null) { @@ -386,73 +389,98 @@ class ChatItem extends StatelessWidget { ? messageContent(context) : isRevoke ? const SizedBox() - : Row( - children: [ - if (!isOwner) const SizedBox(width: 12), - if (isOwner) const Spacer(), - Container( - constraints: const BoxConstraints( - maxWidth: 300.0, // 设置最大宽度为200.0 - ), - decoration: BoxDecoration( - color: isOwner - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.secondaryContainer, - borderRadius: BorderRadius.only( - topLeft: const Radius.circular(16), - topRight: const Radius.circular(16), - bottomLeft: Radius.circular(isOwner ? 16 : 6), - bottomRight: Radius.circular(isOwner ? 6 : 16), + : Padding( + padding: const EdgeInsets.only(top: 12), + child: Row( + mainAxisAlignment: !isOwner + ? MainAxisAlignment.start + : MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(width: safeDistanceval), + if (isOwner) + Text( + Utils.dateFormat(item.timestamp), + style: Theme.of(context).textTheme.labelSmall!.copyWith( + color: Theme.of(context).colorScheme.outline), ), + Container( + constraints: const BoxConstraints( + maxWidth: 300.0, // 设置最大宽度为200.0 + ), + decoration: BoxDecoration( + color: isOwner + ? Theme.of(context) + .colorScheme + .primary + .withAlpha(180) + : Theme.of(context) + .colorScheme + .outlineVariant + .withOpacity(0.6) + .withAlpha(125), + borderRadius: BorderRadius.only( + topLeft: const Radius.circular(borderRadiusVal), + topRight: const Radius.circular(borderRadiusVal), + bottomLeft: + Radius.circular(isOwner ? borderRadiusVal : 2), + bottomRight: + Radius.circular(isOwner ? 2 : borderRadiusVal), + ), + ), + margin: const EdgeInsets.only( + left: 8, + right: 8, + ), + padding: const EdgeInsets.all(paddingVal), + child: messageContent(context), + // child: Column( + // crossAxisAlignment: isOwner + // ? CrossAxisAlignment.end + // : CrossAxisAlignment.start, + // children: [ + // messageContent(context), + // SizedBox(height: isPic ? 7 : 2), + // Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // Text( + // Utils.dateFormat(item.timestamp), + // style: Theme.of(context) + // .textTheme + // .labelSmall! + // .copyWith( + // color: isOwner + // ? Theme.of(context) + // .colorScheme + // .onPrimary + // .withOpacity(0.8) + // : Theme.of(context) + // .colorScheme + // .onSecondaryContainer + // .withOpacity(0.8)), + // ), + // item.msgStatus == 1 + // ? Text( + // ' 已撤回', + // style: + // Theme.of(context).textTheme.labelSmall!, + // ) + // : const SizedBox() + // ], + // ) + // ], + // ), ), - margin: const EdgeInsets.only(top: 12), - padding: EdgeInsets.only( - top: 8, - bottom: 6, - left: isPic ? 8 : 12, - right: isPic ? 8 : 12, - ), - child: Column( - crossAxisAlignment: isOwner - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - messageContent(context), - SizedBox(height: isPic ? 7 : 2), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - Utils.dateFormat(item.timestamp), - style: Theme.of(context) - .textTheme - .labelSmall! - .copyWith( - color: isOwner - ? Theme.of(context) - .colorScheme - .onPrimary - .withOpacity(0.8) - : Theme.of(context) - .colorScheme - .onSecondaryContainer - .withOpacity(0.8)), - ), - item.msgStatus == 1 - ? Text( - ' 已撤回', - style: - Theme.of(context).textTheme.labelSmall!, - ) - : const SizedBox() - ], - ) - ], - ), - ), - if (!isOwner) const Spacer(), - if (isOwner) const SizedBox(width: 12), - ], + if (!isOwner) + Text( + Utils.dateFormat(item.timestamp), + style: Theme.of(context).textTheme.labelSmall!.copyWith( + color: Theme.of(context).colorScheme.outline), + ), + const SizedBox(width: safeDistanceval), + ], + ), ); } } From 46cef5e55b41adc0c9448255afc79051c3c44622 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sat, 15 Jun 2024 17:03:52 +0800 Subject: [PATCH 02/19] =?UTF-8?q?feat:=20=E4=BC=9A=E8=AF=9D=E7=A7=BB?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/http/api.dart | 4 ++ lib/http/msg.dart | 31 +++++++++++---- lib/pages/whisper/controller.dart | 6 +++ lib/pages/whisper/view.dart | 18 +++++---- lib/pages/whisper_detail/controller.dart | 38 +++++++++++++++++++ lib/pages/whisper_detail/view.dart | 16 ++++---- .../whisper_detail/widget/chat_item.dart | 15 +++++++- 7 files changed, 105 insertions(+), 23 deletions(-) diff --git a/lib/http/api.dart b/lib/http/api.dart index e519d91c..aaa804fa 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -535,4 +535,8 @@ class Api { /// 搜索结果计数 static const String searchCount = '/x/web-interface/wbi/search/all/v2'; + + /// 关闭会话 + static const String removeSession = + '${HttpString.tUrl}/session_svr/v1/session_svr/remove_session'; } diff --git a/lib/http/msg.dart b/lib/http/msg.dart index 7ac30bc1..9cdc9160 100644 --- a/lib/http/msg.dart +++ b/lib/http/msg.dart @@ -124,13 +124,7 @@ class MsgHttp { 'data': res.data['data'], }; } else { - return { - 'status': false, - 'date': [], - 'msg': "message: ${res.data['message']}," - " msg: ${res.data['msg']}," - " code: ${res.data['code']}", - }; + return {'status': false, 'date': [], 'msg': res.data['message']}; } } @@ -208,4 +202,27 @@ class MsgHttp { } return s.join(); } + + static Future removeSession({ + int? talkerId, + }) async { + String csrf = await Request.getCsrf(); + Map params = await WbiSign().makSign({ + 'talker_id': talkerId, + 'session_type': 1, + 'build': 0, + 'mobi_app': 'web', + 'csrf_token': csrf, + 'csrf': csrf + }); + var res = await Request().get(Api.removeSession, data: params); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return {'status': false, 'date': [], 'msg': res.data['message']}; + } + } } diff --git a/lib/pages/whisper/controller.dart b/lib/pages/whisper/controller.dart index b7c52d2e..195b238b 100644 --- a/lib/pages/whisper/controller.dart +++ b/lib/pages/whisper/controller.dart @@ -71,4 +71,10 @@ class WhisperController extends GetxController { sessionList.insert(0, currentItem); sessionList.refresh(); } + + // 移除会话 + void removeSessionMsg(int talkerId) { + sessionList.removeWhere((p0) => p0.talkerId == talkerId); + sessionList.refresh(); + } } diff --git a/lib/pages/whisper/view.dart b/lib/pages/whisper/view.dart index 6d2f281b..bbe17048 100644 --- a/lib/pages/whisper/view.dart +++ b/lib/pages/whisper/view.dart @@ -204,6 +204,8 @@ class SessionItem extends StatelessWidget { Widget build(BuildContext context) { final String heroTag = Utils.makeHeroTag(sessionItem.accountInfo.mid); final content = sessionItem.lastMsg.content; + final msgStatus = sessionItem.lastMsg.msgStatus; + return ListTile( onTap: () { sessionItem.unreadCount = 0; @@ -235,13 +237,15 @@ class SessionItem extends StatelessWidget { ), title: Text(sessionItem.accountInfo.name), subtitle: Text( - content != null && content != '' - ? (content['text'] ?? - content['content'] ?? - content['title'] ?? - content['reply_content'] ?? - '不支持的消息类型') - : '不支持的消息类型', + msgStatus == 1 + ? '你撤回了一条消息' + : content != null && content != '' + ? (content['text'] ?? + content['content'] ?? + content['title'] ?? + content['reply_content'] ?? + '不支持的消息类型') + : '不支持的消息类型', maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context) diff --git a/lib/pages/whisper_detail/controller.dart b/lib/pages/whisper_detail/controller.dart index 3c7e0837..32e0ceb0 100644 --- a/lib/pages/whisper_detail/controller.dart +++ b/lib/pages/whisper_detail/controller.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -105,4 +106,41 @@ class WhisperDetailController extends GetxController { SmartDialog.showToast(result['msg']); } } + + void removeSession(context) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + clipBehavior: Clip.hardEdge, + title: const Text('提示'), + content: const Text('确认清空会话内容并移除会话?'), + actions: [ + TextButton( + onPressed: Get.back, + child: Text( + '取消', + style: TextStyle(color: Theme.of(context).colorScheme.outline), + ), + ), + TextButton( + onPressed: () async { + var res = await MsgHttp.removeSession(talkerId: talkerId); + if (res['status']) { + SmartDialog.showToast('操作成功'); + try { + late final WhisperController whisperController = + Get.find(); + whisperController.removeSessionMsg(talkerId!); + Get.back(); + } catch (_) {} + } + }, + child: const Text('确认'), + ), + ], + ); + }, + ); + } } diff --git a/lib/pages/whisper_detail/view.dart b/lib/pages/whisper_detail/view.dart index 7c5762d9..912b5dc5 100644 --- a/lib/pages/whisper_detail/view.dart +++ b/lib/pages/whisper_detail/view.dart @@ -145,7 +145,7 @@ class _WhisperDetailPageState extends State src: _whisperDetailController.face, ), ), - const SizedBox(width: 6), + const SizedBox(width: 10), Text( _whisperDetailController.name, style: Theme.of(context).textTheme.titleMedium, @@ -158,12 +158,14 @@ class _WhisperDetailPageState extends State ), ), actions: [ - IconButton( - onPressed: () {}, - icon: const Icon( - Icons.more_vert_outlined, - size: 20, - ), + PopupMenuButton( + icon: const Icon(Icons.more_vert_outlined, size: 20), + itemBuilder: (BuildContext context) => [ + PopupMenuItem( + onTap: () => _whisperDetailController.removeSession(context), + child: const Text('关闭会话'), + ) + ], ), const SizedBox(width: 14) ], diff --git a/lib/pages/whisper_detail/widget/chat_item.dart b/lib/pages/whisper_detail/widget/chat_item.dart index c0d87221..f64cf223 100644 --- a/lib/pages/whisper_detail/widget/chat_item.dart +++ b/lib/pages/whisper_detail/widget/chat_item.dart @@ -389,8 +389,19 @@ class ChatItem extends StatelessWidget { ? messageContent(context) : isRevoke ? const SizedBox() - : Padding( - padding: const EdgeInsets.only(top: 12), + : Container( + padding: const EdgeInsets.only(top: 6, bottom: 6), + decoration: BoxDecoration( + border: Border( + left: item.msgStatus == 1 && !isOwner + ? BorderSide( + width: 4, color: Theme.of(context).dividerColor) + : BorderSide.none, + right: item.msgStatus == 1 && isOwner + ? BorderSide( + width: 4, color: Theme.of(context).primaryColor) + : BorderSide.none, + )), child: Row( mainAxisAlignment: !isOwner ? MainAxisAlignment.start From bfdd996b0833f13ad7b4de6b1ee11850ba8e4fbd Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sat, 15 Jun 2024 20:34:28 +0800 Subject: [PATCH 03/19] =?UTF-8?q?feat:=20=E6=9C=AA=E8=AF=BB=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E8=AE=A1=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/http/api.dart | 3 + lib/http/msg.dart | 12 ++ lib/pages/whisper/controller.dart | 45 +++++ lib/pages/whisper/view.dart | 269 +++++++++++++++--------------- 4 files changed, 194 insertions(+), 135 deletions(-) diff --git a/lib/http/api.dart b/lib/http/api.dart index aaa804fa..198d6174 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -539,4 +539,7 @@ class Api { /// 关闭会话 static const String removeSession = '${HttpString.tUrl}/session_svr/v1/session_svr/remove_session'; + + /// 消息未读数 + static const String unread = '${HttpString.tUrl}/x/im/web/msgfeed/unread'; } diff --git a/lib/http/msg.dart b/lib/http/msg.dart index 9cdc9160..9a6e878b 100644 --- a/lib/http/msg.dart +++ b/lib/http/msg.dart @@ -225,4 +225,16 @@ class MsgHttp { return {'status': false, 'date': [], 'msg': res.data['message']}; } } + + static Future unread() async { + var res = await Request().get(Api.unread); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return {'status': false, 'date': [], 'msg': res.data['message']}; + } + } } diff --git a/lib/pages/whisper/controller.dart b/lib/pages/whisper/controller.dart index 195b238b..e00c990e 100644 --- a/lib/pages/whisper/controller.dart +++ b/lib/pages/whisper/controller.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/http/msg.dart'; import 'package:pilipala/models/msg/account.dart'; @@ -7,6 +8,38 @@ class WhisperController extends GetxController { RxList sessionList = [].obs; RxList accountList = [].obs; bool isLoading = false; + RxList noticesList = [ + { + 'icon': Icons.message_outlined, + 'title': '回复我的', + 'path': '', + 'count': 0, + }, + { + 'icon': Icons.alternate_email, + 'title': '@ 我的', + 'path': '', + 'count': 0, + }, + { + 'icon': Icons.thumb_up_outlined, + 'title': '收到的赞', + 'path': '', + 'count': 0, + }, + { + 'icon': Icons.notifications_none_outlined, + 'title': '系统通知', + 'path': '', + 'count': 0, + } + ].obs; + + @override + void onInit() { + unread(); + super.onInit(); + } Future querySessionList(String? type) async { if (isLoading) return; @@ -77,4 +110,16 @@ class WhisperController extends GetxController { sessionList.removeWhere((p0) => p0.talkerId == talkerId); sessionList.refresh(); } + + // 消息未读数 + void unread() async { + var res = await MsgHttp.unread(); + if (res['status']) { + noticesList[0]['count'] = res['data']['reply']; + noticesList[1]['count'] = res['data']['at']; + noticesList[2]['count'] = res['data']['like']; + noticesList[3]['count'] = res['data']['sys_msg']; + noticesList.refresh(); + } + } } diff --git a/lib/pages/whisper/view.dart b/lib/pages/whisper/view.dart index bbe17048..8070498e 100644 --- a/lib/pages/whisper/view.dart +++ b/lib/pages/whisper/view.dart @@ -1,6 +1,7 @@ import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/skeleton/skeleton.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/utils/utils.dart'; @@ -44,147 +45,145 @@ class _WhisperPageState extends State { appBar: AppBar( title: const Text('消息'), ), - body: Column( - children: [ - // LayoutBuilder( - // builder: (BuildContext context, BoxConstraints constraints) { - // // 在这里根据父级容器的约束条件构建小部件树 - // return Padding( - // padding: const EdgeInsets.only(left: 20, right: 20), - // child: SizedBox( - // height: constraints.maxWidth / 5, - // child: GridView.count( - // primary: false, - // crossAxisCount: 4, - // padding: const EdgeInsets.all(0), - // childAspectRatio: 1.25, - // children: [ - // Column( - // crossAxisAlignment: CrossAxisAlignment.center, - // mainAxisAlignment: MainAxisAlignment.center, - // children: [ - // SizedBox( - // width: 36, - // height: 36, - // child: IconButton( - // style: ButtonStyle( - // padding: - // MaterialStateProperty.all(EdgeInsets.zero), - // backgroundColor: - // MaterialStateProperty.resolveWith((states) { - // return Theme.of(context) - // .colorScheme - // .primary - // .withOpacity(0.1); - // }), - // ), - // onPressed: () {}, - // icon: Icon( - // Icons.message_outlined, - // size: 18, - // color: Theme.of(context).colorScheme.primary, - // ), - // ), - // ), - // const SizedBox(height: 6), - // const Text('回复我的', style: TextStyle(fontSize: 13)) - // ], - // ), - // ], - // ), - // ), - // ); - // }, - // ), - Expanded( - child: RefreshIndicator( - onRefresh: () async { - await _whisperController.onRefresh(); - }, - child: SingleChildScrollView( - controller: _scrollController, - child: FutureBuilder( - future: _futureBuilderFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - Map? data = snapshot.data; - if (data != null && data['status']) { - RxList sessionList = _whisperController.sessionList; - return Obx( - () => sessionList.isEmpty - ? const SizedBox() - : ListView.separated( - itemCount: sessionList.length, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (_, int i) { - return SessionItem( - sessionItem: sessionList[i], - changeFucCall: () => - sessionList.refresh(), - ); - }, - separatorBuilder: - (BuildContext context, int index) { - return Divider( - indent: 72, - endIndent: 20, - height: 6, - color: Colors.grey.withOpacity(0.1), - ); - }, + body: RefreshIndicator( + onRefresh: () async { + _whisperController.unread(); + await _whisperController.onRefresh(); + }, + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + children: [ + LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + // 在这里根据父级容器的约束条件构建小部件树 + return Padding( + padding: const EdgeInsets.only(left: 20, right: 20), + child: SizedBox( + height: constraints.maxWidth / 4, + child: Obx( + () => GridView.count( + primary: false, + crossAxisCount: 4, + padding: const EdgeInsets.all(0), + children: [ + ..._whisperController.noticesList.map((element) { + return InkWell( + onTap: () => {}, + onLongPress: () {}, + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Badge( + isLabelVisible: element['count'] > 0, + label: Text(element['count'] > 99 + ? '99+' + : element['count'].toString()), + child: Padding( + padding: const EdgeInsets.all(10), + child: Icon( + element['icon'], + size: 21, + color: Theme.of(context) + .colorScheme + .primary, + ), + ), + ), + const SizedBox(height: 4), + Text(element['title']) + ], ), - ); - } else { - // 请求错误 - return Center( - child: Text(data?['msg'] ?? '请求异常'), - ); - } + ); + }).toList(), + ], + ), + ), + ), + ); + }, + ), + FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map? data = snapshot.data; + if (data != null && data['status']) { + RxList sessionList = _whisperController.sessionList; + return Obx( + () => sessionList.isEmpty + ? const SizedBox() + : ListView.separated( + itemCount: sessionList.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (_, int i) { + return SessionItem( + sessionItem: sessionList[i], + changeFucCall: () => sessionList.refresh(), + ); + }, + separatorBuilder: + (BuildContext context, int index) { + return Divider( + indent: 72, + endIndent: 20, + height: 6, + color: Colors.grey.withOpacity(0.1), + ); + }, + ), + ); } else { - // 骨架屏 - return ListView.builder( - itemCount: 15, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, int i) { - return Skeleton( - child: ListTile( - leading: Container( - width: 45, - height: 45, - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .onInverseSurface, - borderRadius: BorderRadius.circular(25), - ), - ), - title: Container( - width: 100, - height: 14, - color: Theme.of(context) - .colorScheme - .onInverseSurface, - ), - subtitle: Container( - width: 80, - height: 14, - color: Theme.of(context) - .colorScheme - .onInverseSurface, - ), - ), - ); - }, + // 请求错误 + return Center( + child: Text(data?['msg'] ?? '请求异常'), ); } - }, - ), + } else { + // 骨架屏 + return ListView.builder( + itemCount: 15, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, int i) { + return Skeleton( + child: ListTile( + leading: Container( + width: 45, + height: 45, + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .onInverseSurface, + borderRadius: BorderRadius.circular(25), + ), + ), + title: Container( + width: 100, + height: 14, + color: Theme.of(context) + .colorScheme + .onInverseSurface, + ), + subtitle: Container( + width: 80, + height: 14, + color: Theme.of(context) + .colorScheme + .onInverseSurface, + ), + ), + ); + }, + ); + } + }, ), - ), + ], ), - ], + ), ), ); } From 569f7c23fd2e64072fe905dbd0d098b1ebc66ec9 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sun, 16 Jun 2024 13:58:43 +0800 Subject: [PATCH 04/19] =?UTF-8?q?feat:=20=E5=9B=9E=E5=A4=8D=E6=88=91?= =?UTF-8?q?=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/http/api.dart | 3 + lib/http/msg.dart | 26 +++ lib/models/msg/reply.dart | 168 ++++++++++++++ lib/pages/message/reply/controller.dart | 25 +++ lib/pages/message/reply/index.dart | 4 + lib/pages/message/reply/view.dart | 285 ++++++++++++++++++++++++ lib/pages/whisper/controller.dart | 2 +- lib/pages/whisper/view.dart | 2 +- lib/router/app_pages.dart | 3 + 9 files changed, 516 insertions(+), 2 deletions(-) create mode 100644 lib/models/msg/reply.dart create mode 100644 lib/pages/message/reply/controller.dart create mode 100644 lib/pages/message/reply/index.dart create mode 100644 lib/pages/message/reply/view.dart diff --git a/lib/http/api.dart b/lib/http/api.dart index 198d6174..4db5994d 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -542,4 +542,7 @@ class Api { /// 消息未读数 static const String unread = '${HttpString.tUrl}/x/im/web/msgfeed/unread'; + + /// 回复我的 + static const String messageReplyAPi = '/x/msgfeed/reply'; } diff --git a/lib/http/msg.dart b/lib/http/msg.dart index 9a6e878b..20905386 100644 --- a/lib/http/msg.dart +++ b/lib/http/msg.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:math'; import 'package:dio/dio.dart'; +import 'package:pilipala/models/msg/reply.dart'; import '../models/msg/account.dart'; import '../models/msg/session.dart'; import '../utils/wbi_sign.dart'; @@ -237,4 +238,29 @@ class MsgHttp { return {'status': false, 'date': [], 'msg': res.data['message']}; } } + + // 回复我的 + static Future messageReply({ + int? id, + int? replyTime, + }) async { + var params = { + if (id != null) 'id': id, + if (replyTime != null) 'reply_time': replyTime, + }; + var res = await Request().get(Api.messageReplyAPi, data: params); + if (res.data['code'] == 0) { + try { + return { + 'status': true, + 'data': MessageReplyModel.fromJson(res.data['data']), + }; + } catch (err) { + print(err); + return {'status': false, 'date': [], 'msg': err.toString()}; + } + } else { + return {'status': false, 'date': [], 'msg': res.data['message']}; + } + } } diff --git a/lib/models/msg/reply.dart b/lib/models/msg/reply.dart new file mode 100644 index 00000000..be617088 --- /dev/null +++ b/lib/models/msg/reply.dart @@ -0,0 +1,168 @@ +class MessageReplyModel { + MessageReplyModel({ + this.cursor, + this.items, + }); + + Cursor? cursor; + List? items; + + MessageReplyModel.fromJson(Map json) { + cursor = Cursor.fromJson(json['cursor']); + items = json["items"] != null + ? json["items"].map((e) { + return MessageReplyItem.fromJson(e); + }).toList() + : []; + } +} + +class Cursor { + Cursor({ + this.id, + this.isEnd, + this.time, + }); + + int? id; + bool? isEnd; + int? time; + + Cursor.fromJson(Map json) { + id = json['id']; + isEnd = json['is_end']; + time = json['time']; + } +} + +class MessageReplyItem { + MessageReplyItem({ + this.count, + this.id, + this.isMulti, + this.item, + this.replyTime, + this.user, + }); + + int? count; + int? id; + int? isMulti; + ReplyContentItem? item; + int? replyTime; + ReplyUser? user; + + MessageReplyItem.fromJson(Map json) { + count = json['count']; + id = json['id']; + isMulti = json['is_multi']; + item = ReplyContentItem.fromJson(json["item"]); + replyTime = json['reply_time']; + user = ReplyUser.fromJson(json['user']); + } +} + +class ReplyContentItem { + ReplyContentItem({ + this.subjectId, + this.rootId, + this.sourceId, + this.targetId, + this.type, + this.businessId, + this.business, + this.title, + this.desc, + this.image, + this.uri, + this.nativeUri, + this.detailTitle, + this.rootReplyContent, + this.sourceContent, + this.targetReplyContent, + this.atDetails, + this.topicDetails, + this.hideReplyButton, + this.hideLikeButton, + this.likeState, + this.danmu, + this.message, + }); + + int? subjectId; + int? rootId; + int? sourceId; + int? targetId; + String? type; + int? businessId; + String? business; + String? title; + String? desc; + String? image; + String? uri; + String? nativeUri; + String? detailTitle; + String? rootReplyContent; + String? sourceContent; + String? targetReplyContent; + List? atDetails; + List? topicDetails; + bool? hideReplyButton; + bool? hideLikeButton; + int? likeState; + String? danmu; + String? message; + + ReplyContentItem.fromJson(Map json) { + subjectId = json['subject_id']; + rootId = json['root_id']; + sourceId = json['source_id']; + targetId = json['target_id']; + type = json['type']; + businessId = json['business_id']; + business = json['business']; + title = json['title']; + desc = json['desc']; + image = json['image']; + uri = json['uri']; + nativeUri = json['native_uri']; + detailTitle = json['detail_title']; + rootReplyContent = json['root_reply_content']; + sourceContent = json['source_content']; + targetReplyContent = json['target_reply_content']; + atDetails = json['at_details']; + topicDetails = json['topic_details']; + hideReplyButton = json['hide_reply_button']; + hideLikeButton = json['hide_like_button']; + likeState = json['like_state']; + danmu = json['danmu']; + message = json['message']; + } +} + +class ReplyUser { + ReplyUser({ + this.mid, + this.fans, + this.nickname, + this.avatar, + this.midLink, + this.follow, + }); + + int? mid; + int? fans; + String? nickname; + String? avatar; + String? midLink; + bool? follow; + + ReplyUser.fromJson(Map json) { + mid = json['mid']; + fans = json['fans']; + nickname = json['nickname']; + avatar = json['avatar']; + midLink = json['mid_link']; + follow = json['follow']; + } +} diff --git a/lib/pages/message/reply/controller.dart b/lib/pages/message/reply/controller.dart new file mode 100644 index 00000000..d62662df --- /dev/null +++ b/lib/pages/message/reply/controller.dart @@ -0,0 +1,25 @@ +import 'package:get/get.dart'; +import 'package:pilipala/http/msg.dart'; +import 'package:pilipala/models/msg/reply.dart'; + +class MessageReplyController extends GetxController { + Cursor? cursor; + RxList replyItems = [].obs; + + Future queryMessageReply({String type = 'init'}) async { + if (cursor != null && cursor!.isEnd == true) { + return {}; + } + var params = { + if (type == 'onLoad') 'id': cursor!.id, + if (type == 'onLoad') 'replyTime': cursor!.time, + }; + var res = await MsgHttp.messageReply( + id: params['id'], replyTime: params['replyTime']); + if (res['status']) { + cursor = res['data'].cursor; + replyItems.addAll(res['data'].items); + } + return res; + } +} diff --git a/lib/pages/message/reply/index.dart b/lib/pages/message/reply/index.dart new file mode 100644 index 00000000..969d03dd --- /dev/null +++ b/lib/pages/message/reply/index.dart @@ -0,0 +1,4 @@ +library message_reply; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/message/reply/view.dart b/lib/pages/message/reply/view.dart new file mode 100644 index 00000000..c4c02f6c --- /dev/null +++ b/lib/pages/message/reply/view.dart @@ -0,0 +1,285 @@ +import 'package:easy_debounce/easy_throttle.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/http/search.dart'; +import 'package:pilipala/models/msg/reply.dart'; +import 'package:pilipala/utils/utils.dart'; + +import 'controller.dart'; + +class MessageReplyPage extends StatefulWidget { + const MessageReplyPage({super.key}); + + @override + State createState() => _MessageReplyPageState(); +} + +class _MessageReplyPageState extends State { + final MessageReplyController _messageReplyCtr = + Get.put(MessageReplyController()); + late Future _futureBuilderFuture; + final ScrollController scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _futureBuilderFuture = _messageReplyCtr.queryMessageReply(); + scrollController.addListener( + () async { + if (scrollController.position.pixels >= + scrollController.position.maxScrollExtent - 200) { + EasyThrottle.throttle('follow', const Duration(seconds: 1), () { + _messageReplyCtr.queryMessageReply(type: 'onLoad'); + }); + } + }, + ); + } + + @override + void dispose() { + scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('回复我的'), + ), + body: RefreshIndicator( + onRefresh: () async { + await _messageReplyCtr.queryMessageReply(type: 'init'); + }, + child: FutureBuilder( + future: _futureBuilderFuture, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.data == null) { + return const SizedBox(); + } + if (snapshot.data['status']) { + final replyItems = _messageReplyCtr.replyItems; + return Obx( + () => ListView.separated( + controller: scrollController, + itemBuilder: (context, index) => + ReplyItem(item: replyItems[index]), + itemCount: replyItems.length, + separatorBuilder: (BuildContext context, int index) { + return Divider( + indent: 66, + endIndent: 14, + height: 1, + color: Colors.grey.withOpacity(0.1), + ); + }, + ), + ); + } else { + // 请求错误 + return CustomScrollView( + slivers: [ + HttpError( + errMsg: snapshot.data['msg'], + fn: () { + setState(() { + _futureBuilderFuture = + _messageReplyCtr.queryMessageReply(); + }); + }, + ) + ], + ); + } + } else { + return const SizedBox(); + } + }, + ), + ), + ); + } +} + +class ReplyItem extends StatelessWidget { + final MessageReplyItem item; + + const ReplyItem({super.key, required this.item}); + + @override + Widget build(BuildContext context) { + final String heroTag = Utils.makeHeroTag(item.user!.mid); + final String bvid = item.item!.uri!.split('/').last; + // 页码 + final String page = + item.item!.nativeUri!.split('page=').last.split('&').first; + // 根评论id + final String commentRootId = + item.item!.nativeUri!.split('comment_root_id=').last.split('&').first; + // 二级评论id + final String commentSecondaryId = + item.item!.nativeUri!.split('comment_secondary_id=').last; + + return InkWell( + onTap: () async { + final int cid = await SearchHttp.ab2c(bvid: bvid); + final String heroTag = Utils.makeHeroTag(bvid); + Get.toNamed( + '/video?bvid=$bvid&cid=$cid', + arguments: { + 'pic': '', + 'heroTag': heroTag, + }, + ); + }, + child: Padding( + padding: const EdgeInsets.all(14), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () { + Get.toNamed('/member?mid=${item.user!.mid}', + arguments: {'face': item.user!.avatar, 'heroTag': heroTag}); + }, + child: Hero( + tag: heroTag, + child: NetworkImgLayer( + width: 42, + height: 42, + type: 'avatar', + src: item.user!.avatar, + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text.rich(TextSpan(children: [ + TextSpan(text: item.user!.nickname!), + const TextSpan(text: ' '), + if (item.item!.type! == 'video') + TextSpan( + text: '对我的视频发表了评论', + style: TextStyle( + color: Theme.of(context).colorScheme.outline), + ), + if (item.item!.type! == 'reply') + TextSpan( + text: '回复了我的评论', + style: TextStyle( + color: Theme.of(context).colorScheme.outline), + ), + ])), + const SizedBox(height: 6), + Text.rich( + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(letterSpacing: 0.3), + buildContent(context, item.item)), + if (item.item!.targetReplyContent != '') ...[ + const SizedBox(height: 2), + Text( + item.item!.targetReplyContent!, + style: TextStyle( + color: Theme.of(context).colorScheme.outline), + ), + ], + const SizedBox(height: 4), + Row( + children: [ + Text( + Utils.dateFormat(item.replyTime!, formatType: 'detail'), + style: TextStyle( + color: Theme.of(context).colorScheme.outline), + ), + const SizedBox(width: 16), + Text( + '回复', + style: TextStyle( + color: Theme.of(context).colorScheme.outline), + ), + ], + ) + ], + ), + ), + // Spacer(), + const SizedBox(width: 25), + if (item.item!.type! == 'reply') + Container( + width: 60, + height: 80, + padding: const EdgeInsets.all(4), + child: Text( + item.item!.rootReplyContent!, + maxLines: 4, + style: const TextStyle( + fontSize: 12, + letterSpacing: 0.3, + ), + overflow: TextOverflow.ellipsis, + ), + ), + if (item.item!.type! == 'video') + NetworkImgLayer( + width: 60, + height: 60, + src: item.item!.image, + ), + ], + ), + ), + ); + } +} + +InlineSpan buildContent(BuildContext context, item) { + List? atDetails = item!.atDetails; + final List spanChilds = []; + if (atDetails!.isNotEmpty) { + final String patternStr = + atDetails.map((e) => '@${e['nickname']}').toList().join('|'); + final RegExp regExp = RegExp(patternStr); + item.sourceContent!.splitMapJoin( + regExp, + onMatch: (Match match) { + spanChilds.add( + TextSpan( + text: match.group(0), + recognizer: TapGestureRecognizer() + ..onTap = () { + var currentUser = atDetails + .where((e) => e['nickname'] == match.group(0)!.substring(1)) + .first; + Get.toNamed('/member?mid=${currentUser['mid']}', arguments: { + 'face': currentUser['avatar'], + }); + }, + style: TextStyle(color: Theme.of(context).colorScheme.primary), + ), + ); + return ''; + }, + onNonMatch: (String nonMatch) { + spanChilds.add( + TextSpan(text: nonMatch), + ); + return ''; + }, + ); + } else { + spanChilds.add( + TextSpan(text: item.sourceContent), + ); + } + + return TextSpan(children: spanChilds); +} diff --git a/lib/pages/whisper/controller.dart b/lib/pages/whisper/controller.dart index e00c990e..f3cc47d6 100644 --- a/lib/pages/whisper/controller.dart +++ b/lib/pages/whisper/controller.dart @@ -12,7 +12,7 @@ class WhisperController extends GetxController { { 'icon': Icons.message_outlined, 'title': '回复我的', - 'path': '', + 'path': '/messageReply', 'count': 0, }, { diff --git a/lib/pages/whisper/view.dart b/lib/pages/whisper/view.dart index 8070498e..fccdd844 100644 --- a/lib/pages/whisper/view.dart +++ b/lib/pages/whisper/view.dart @@ -69,7 +69,7 @@ class _WhisperPageState extends State { children: [ ..._whisperController.noticesList.map((element) { return InkWell( - onTap: () => {}, + onTap: () => Get.toNamed(element['path']), onLongPress: () {}, borderRadius: StyleString.mdRadius, child: Column( diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 2ca333f8..bb036c6e 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/pages/follow_search/view.dart'; +import 'package:pilipala/pages/message/reply/index.dart'; import 'package:pilipala/pages/setting/pages/logs.dart'; import '../pages/about/index.dart'; @@ -178,6 +179,8 @@ class Routes { // 操作菜单 CustomGetPage( name: '/actionMenuSet', page: () => const ActionMenuSetPage()), + // 回复我的 + CustomGetPage(name: '/messageReply', page: () => const MessageReplyPage()), ]; } From 779f7e76662fbadc562bff5f13c3b5c0f4858386 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sun, 16 Jun 2024 17:53:46 +0800 Subject: [PATCH 05/19] =?UTF-8?q?feat:=20=E6=94=B6=E5=88=B0=E7=9A=84?= =?UTF-8?q?=E8=B5=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/http/api.dart | 3 + lib/http/msg.dart | 26 +- lib/models/msg/like.dart | 183 +++++++++++++ lib/pages/message/at/controller.dart | 3 + lib/pages/message/at/index.dart | 4 + lib/pages/message/at/view.dart | 19 ++ lib/pages/message/like/controller.dart | 30 +++ lib/pages/message/like/index.dart | 4 + lib/pages/message/like/view.dart | 319 +++++++++++++++++++++++ lib/pages/message/reply/view.dart | 27 +- lib/pages/message/system/controller.dart | 3 + lib/pages/message/system/index.dart | 4 + lib/pages/message/system/view.dart | 19 ++ lib/pages/whisper/controller.dart | 6 +- lib/router/app_pages.dart | 10 + 15 files changed, 636 insertions(+), 24 deletions(-) create mode 100644 lib/models/msg/like.dart create mode 100644 lib/pages/message/at/controller.dart create mode 100644 lib/pages/message/at/index.dart create mode 100644 lib/pages/message/at/view.dart create mode 100644 lib/pages/message/like/controller.dart create mode 100644 lib/pages/message/like/index.dart create mode 100644 lib/pages/message/like/view.dart create mode 100644 lib/pages/message/system/controller.dart create mode 100644 lib/pages/message/system/index.dart create mode 100644 lib/pages/message/system/view.dart diff --git a/lib/http/api.dart b/lib/http/api.dart index 4db5994d..f20b8bcf 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -545,4 +545,7 @@ class Api { /// 回复我的 static const String messageReplyAPi = '/x/msgfeed/reply'; + + /// 收到的赞 + static const String messageLikeAPi = '/x/msgfeed/like'; } diff --git a/lib/http/msg.dart b/lib/http/msg.dart index 20905386..7c168230 100644 --- a/lib/http/msg.dart +++ b/lib/http/msg.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:math'; import 'package:dio/dio.dart'; +import 'package:pilipala/models/msg/like.dart'; import 'package:pilipala/models/msg/reply.dart'; import '../models/msg/account.dart'; import '../models/msg/session.dart'; @@ -256,7 +257,30 @@ class MsgHttp { 'data': MessageReplyModel.fromJson(res.data['data']), }; } catch (err) { - print(err); + return {'status': false, 'date': [], 'msg': err.toString()}; + } + } else { + return {'status': false, 'date': [], 'msg': res.data['message']}; + } + } + + // 收到的赞 + static Future messageLike({ + int? id, + int? likeTime, + }) async { + var params = { + if (id != null) 'id': id, + if (likeTime != null) 'like_time': likeTime, + }; + var res = await Request().get(Api.messageLikeAPi, data: params); + if (res.data['code'] == 0) { + try { + return { + 'status': true, + 'data': MessageLikeModel.fromJson(res.data['data']), + }; + } catch (err) { return {'status': false, 'date': [], 'msg': err.toString()}; } } else { diff --git a/lib/models/msg/like.dart b/lib/models/msg/like.dart new file mode 100644 index 00000000..b279131b --- /dev/null +++ b/lib/models/msg/like.dart @@ -0,0 +1,183 @@ +class MessageLikeModel { + MessageLikeModel({ + this.latest, + this.total, + }); + + Latest? latest; + Total? total; + + factory MessageLikeModel.fromJson(Map json) => + MessageLikeModel( + latest: json["latest"] == null ? null : Latest.fromJson(json["latest"]), + total: json["total"] == null ? null : Total.fromJson(json["total"]), + ); +} + +class Latest { + Latest({ + this.items, + this.lastViewAt, + }); + + List? items; + int? lastViewAt; + + factory Latest.fromJson(Map json) => Latest( + items: json["items"], + lastViewAt: json["last_view_at"], + ); +} + +class Total { + Total({ + this.cursor, + this.items, + }); + + Cursor? cursor; + List? items; + + factory Total.fromJson(Map json) => Total( + cursor: Cursor.fromJson(json['cursor']), + items: json["items"] == null + ? [] + : json["items"].map((e) { + return MessageLikeItem.fromJson(e); + }).toList(), + ); +} + +class Cursor { + Cursor({ + this.id, + this.isEnd, + this.time, + }); + + int? id; + bool? isEnd; + int? time; + + factory Cursor.fromJson(Map json) => Cursor( + id: json['id'], + isEnd: json['is_end'], + time: json['time'], + ); +} + +class MessageLikeItem { + MessageLikeItem({ + this.id, + this.users, + this.item, + this.counts, + this.likeTime, + this.noticeState, + this.isExpand = false, + }); + + int? id; + List? users; + MessageLikeItemItem? item; + int? counts; + int? likeTime; + int? noticeState; + bool isExpand; + + factory MessageLikeItem.fromJson(Map json) => + MessageLikeItem( + id: json["id"], + users: json["users"] == null + ? [] + : json["users"].map((e) { + return MessageLikeUser.fromJson(e); + }).toList(), + item: json["item"] == null + ? null + : MessageLikeItemItem.fromJson(json["item"]), + counts: json["counts"], + likeTime: json["like_time"], + noticeState: json["notice_state"], + ); +} + +class MessageLikeUser { + MessageLikeUser({ + this.mid, + this.fans, + this.nickname, + this.avatar, + this.midLink, + this.follow, + }); + + int? mid; + int? fans; + String? nickname; + String? avatar; + String? midLink; + bool? follow; + + factory MessageLikeUser.fromJson(Map json) => + MessageLikeUser( + mid: json["mid"], + fans: json["fans"], + nickname: json["nickname"], + avatar: json["avatar"], + midLink: json["mid_link"], + follow: json["follow"], + ); +} + +class MessageLikeItemItem { + MessageLikeItemItem({ + this.itemId, + this.pid, + this.type, + this.business, + this.businessId, + this.replyBusinessId, + this.likeBusinessId, + this.title, + this.desc, + this.image, + this.uri, + this.detailName, + this.nativeUri, + this.ctime, + }); + + int? itemId; + int? pid; + String? type; + String? business; + int? businessId; + int? replyBusinessId; + int? likeBusinessId; + String? title; + String? desc; + String? image; + String? uri; + String? detailName; + String? nativeUri; + int? ctime; + + factory MessageLikeItemItem.fromJson(Map json) => + MessageLikeItemItem( + itemId: json["item_id"], + pid: json["pid"], + type: json["type"], + business: json["business"], + businessId: json["business_id"], + replyBusinessId: json["reply_business_id"], + likeBusinessId: json["like_business_id"], + title: json["title"], + desc: json["desc"], + image: json["image"], + uri: json["uri"], + detailName: json["detail_name"], + nativeUri: json["native_uri"], + ctime: json["ctime"], + ); +} diff --git a/lib/pages/message/at/controller.dart b/lib/pages/message/at/controller.dart new file mode 100644 index 00000000..af08987f --- /dev/null +++ b/lib/pages/message/at/controller.dart @@ -0,0 +1,3 @@ +import 'package:get/get.dart'; + +class MessageAtController extends GetxController {} diff --git a/lib/pages/message/at/index.dart b/lib/pages/message/at/index.dart new file mode 100644 index 00000000..b2c573e6 --- /dev/null +++ b/lib/pages/message/at/index.dart @@ -0,0 +1,4 @@ +library message_at; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/message/at/view.dart b/lib/pages/message/at/view.dart new file mode 100644 index 00000000..9c48ec99 --- /dev/null +++ b/lib/pages/message/at/view.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class MessageAtPage extends StatefulWidget { + const MessageAtPage({super.key}); + + @override + State createState() => _MessageAtPageState(); +} + +class _MessageAtPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('@我的'), + ), + ); + } +} diff --git a/lib/pages/message/like/controller.dart b/lib/pages/message/like/controller.dart new file mode 100644 index 00000000..9b09f89a --- /dev/null +++ b/lib/pages/message/like/controller.dart @@ -0,0 +1,30 @@ +import 'package:get/get.dart'; +import 'package:pilipala/http/msg.dart'; +import 'package:pilipala/models/msg/like.dart'; + +class MessageLikeController extends GetxController { + Cursor? cursor; + RxList likeItems = [].obs; + + Future queryMessageLike({String type = 'init'}) async { + if (cursor != null && cursor!.isEnd == true) { + return {}; + } + var params = { + if (type == 'onLoad') 'id': cursor!.id, + if (type == 'onLoad') 'likeTime': cursor!.time, + }; + var res = await MsgHttp.messageLike( + id: params['id'], likeTime: params['likeTime']); + if (res['status']) { + cursor = res['data'].total.cursor; + likeItems.addAll(res['data'].total.items); + } + return res; + } + + Future expandedUsersAvatar(i) async { + likeItems[i].isExpand = !likeItems[i].isExpand; + likeItems.refresh(); + } +} diff --git a/lib/pages/message/like/index.dart b/lib/pages/message/like/index.dart new file mode 100644 index 00000000..4f9c143a --- /dev/null +++ b/lib/pages/message/like/index.dart @@ -0,0 +1,4 @@ +library message_like; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/message/like/view.dart b/lib/pages/message/like/view.dart new file mode 100644 index 00000000..e677fb25 --- /dev/null +++ b/lib/pages/message/like/view.dart @@ -0,0 +1,319 @@ +import 'package:easy_debounce/easy_throttle.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/http/search.dart'; +import 'package:pilipala/models/msg/like.dart'; +import 'package:pilipala/utils/utils.dart'; + +import 'controller.dart'; + +class MessageLikePage extends StatefulWidget { + const MessageLikePage({super.key}); + + @override + State createState() => _MessageLikePageState(); +} + +class _MessageLikePageState extends State { + final MessageLikeController _messageLikeCtr = + Get.put(MessageLikeController()); + late Future _futureBuilderFuture; + final ScrollController scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _futureBuilderFuture = _messageLikeCtr.queryMessageLike(); + scrollController.addListener( + () async { + if (scrollController.position.pixels >= + scrollController.position.maxScrollExtent - 200) { + EasyThrottle.throttle('follow', const Duration(seconds: 1), () { + _messageLikeCtr.queryMessageLike(type: 'onLoad'); + }); + } + }, + ); + } + + @override + void dispose() { + scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('收到的赞'), + ), + body: RefreshIndicator( + onRefresh: () async { + await _messageLikeCtr.queryMessageLike(type: 'init'); + }, + child: FutureBuilder( + future: _futureBuilderFuture, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.data == null) { + return const SizedBox(); + } + if (snapshot.data['status']) { + final likeItems = _messageLikeCtr.likeItems; + return Obx( + () => ListView.separated( + controller: scrollController, + itemBuilder: (context, index) => LikeItem( + item: likeItems[index], + index: index, + messageLikeCtr: _messageLikeCtr, + ), + itemCount: likeItems.length, + separatorBuilder: (BuildContext context, int index) { + return Divider( + indent: 66, + endIndent: 14, + height: 1, + color: Colors.grey.withOpacity(0.1), + ); + }, + ), + ); + } else { + // 请求错误 + return CustomScrollView( + slivers: [ + HttpError( + errMsg: snapshot.data['msg'], + fn: () { + setState(() { + _futureBuilderFuture = + _messageLikeCtr.queryMessageLike(); + }); + }, + ) + ], + ); + } + } else { + return const SizedBox(); + } + }, + ), + ), + ); + } +} + +class LikeItem extends StatelessWidget { + final MessageLikeItem item; + final int index; + final MessageLikeController messageLikeCtr; + + const LikeItem( + {super.key, + required this.item, + required this.index, + required this.messageLikeCtr}); + + @override + Widget build(BuildContext context) { + Color outline = Theme.of(context).colorScheme.outline; + final nickNameList = item.users!.map((e) => e.nickname).take(2).toList(); + int usersLen = item.users!.length > 3 ? 3 : item.users!.length; + final String bvid = item.item!.uri!.split('/').last; + // 页码 + final String page = + item.item!.nativeUri!.split('page=').last.split('&').first; + // 根评论id + final String commentRootId = + item.item!.nativeUri!.split('comment_root_id=').last.split('&').first; + // 二级评论id + final String commentSecondaryId = + item.item!.nativeUri!.split('comment_secondary_id=').last; + + return InkWell( + onTap: () async { + try { + final int cid = await SearchHttp.ab2c(bvid: bvid); + final String heroTag = Utils.makeHeroTag(bvid); + Get.toNamed( + '/video?bvid=$bvid&cid=$cid', + arguments: { + 'pic': '', + 'heroTag': heroTag, + }, + ); + } catch (_) { + SmartDialog.showToast('视频可能失效了'); + } + }, + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.all(14), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + if (usersLen == 1) { + final String heroTag = + Utils.makeHeroTag(item.users!.first.mid); + Get.toNamed('/member?mid=${item.users!.first.mid}', + arguments: { + 'face': item.users!.first.avatar, + 'heroTag': heroTag + }); + } else { + messageLikeCtr.expandedUsersAvatar(index); + } + }, + // 多个头像层叠 + child: SizedBox( + width: 50, + height: 50, + child: Stack( + children: [ + for (var i = 0; i < usersLen; i++) + Positioned( + top: i % 2 * (50 / (usersLen >= 2 ? 2 : 1)), + left: i / 2 * (50 / (usersLen >= 2 ? 2 : 1)), + child: NetworkImgLayer( + width: 50 / (usersLen >= 2 ? 2 : 1), + height: 50 / (usersLen >= 2 ? 2 : 1), + type: 'avatar', + src: item.users![i].avatar, + ), + ), + ], + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text.rich(TextSpan(children: [ + TextSpan(text: nickNameList.join('、')), + const TextSpan(text: ' '), + if (item.users!.length > 1) + TextSpan( + text: '等总计${item.users!.length}人', + style: TextStyle(color: outline), + ), + TextSpan( + text: '赞了我的评论', + style: TextStyle(color: outline), + ), + ])), + const SizedBox(height: 4), + Text( + Utils.dateFormat(item.likeTime!, formatType: 'detail'), + style: TextStyle(color: outline), + ), + ], + ), + ), + const SizedBox(width: 25), + if (item.item!.type! == 'reply') + Container( + width: 60, + height: 60, + padding: const EdgeInsets.all(4), + child: Text( + item.item!.title!, + maxLines: 4, + style: const TextStyle(fontSize: 12, letterSpacing: 0.3), + overflow: TextOverflow.ellipsis, + ), + ), + if (item.item!.type! == 'video') + NetworkImgLayer( + width: 60, + height: 60, + src: item.item!.image, + ), + ], + ), + ), + Positioned( + top: 0, + right: 0, + bottom: 0, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: item.isExpand ? Get.size.width - 74 : 0, + color: Theme.of(context).colorScheme.secondaryContainer, + child: ListView.builder( + itemCount: item.users!.length, + scrollDirection: Axis.horizontal, + itemBuilder: (BuildContext context, int i) { + return Padding( + padding: EdgeInsets.fromLTRB(i == 0 ? 12 : 4, 8, 4, 0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: () { + final String heroTag = + Utils.makeHeroTag(item.users![i].mid); + Get.toNamed( + '/member?mid=${item.users![i].mid}', + arguments: { + 'face': item.users![i].avatar, + 'heroTag': heroTag + }, + ); + }, + child: NetworkImgLayer( + width: 42, + height: 42, + type: 'avatar', + src: item.users![i].avatar, + ), + ), + const SizedBox(height: 6), + SizedBox( + width: 68, + child: Text( + textAlign: TextAlign.center, + item.users![i].nickname!, + maxLines: 1, + overflow: TextOverflow.clip, + style: TextStyle(color: outline), + ), + ), + ], + ), + ); + }, + ), + ), + ), + Positioned( + top: 0, + left: 0, + bottom: 0, + child: GestureDetector( + onTap: () { + messageLikeCtr.expandedUsersAvatar(index); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: item.isExpand ? 74 : 0, + color: Colors.black.withOpacity(0.3), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/message/reply/view.dart b/lib/pages/message/reply/view.dart index c4c02f6c..881f8650 100644 --- a/lib/pages/message/reply/view.dart +++ b/lib/pages/message/reply/view.dart @@ -113,6 +113,7 @@ class ReplyItem extends StatelessWidget { @override Widget build(BuildContext context) { + Color outline = Theme.of(context).colorScheme.outline; final String heroTag = Utils.makeHeroTag(item.user!.mid); final String bvid = item.item!.uri!.split('/').last; // 页码 @@ -167,15 +168,11 @@ class ReplyItem extends StatelessWidget { const TextSpan(text: ' '), if (item.item!.type! == 'video') TextSpan( - text: '对我的视频发表了评论', - style: TextStyle( - color: Theme.of(context).colorScheme.outline), - ), + text: '对我的视频发表了评论', style: TextStyle(color: outline)), if (item.item!.type! == 'reply') TextSpan( text: '回复了我的评论', - style: TextStyle( - color: Theme.of(context).colorScheme.outline), + style: TextStyle(color: outline), ), ])), const SizedBox(height: 6), @@ -188,8 +185,7 @@ class ReplyItem extends StatelessWidget { const SizedBox(height: 2), Text( item.item!.targetReplyContent!, - style: TextStyle( - color: Theme.of(context).colorScheme.outline), + style: TextStyle(color: outline), ), ], const SizedBox(height: 4), @@ -197,21 +193,15 @@ class ReplyItem extends StatelessWidget { children: [ Text( Utils.dateFormat(item.replyTime!, formatType: 'detail'), - style: TextStyle( - color: Theme.of(context).colorScheme.outline), + style: TextStyle(color: outline), ), const SizedBox(width: 16), - Text( - '回复', - style: TextStyle( - color: Theme.of(context).colorScheme.outline), - ), + Text('回复', style: TextStyle(color: outline)), ], ) ], ), ), - // Spacer(), const SizedBox(width: 25), if (item.item!.type! == 'reply') Container( @@ -221,10 +211,7 @@ class ReplyItem extends StatelessWidget { child: Text( item.item!.rootReplyContent!, maxLines: 4, - style: const TextStyle( - fontSize: 12, - letterSpacing: 0.3, - ), + style: const TextStyle(fontSize: 12, letterSpacing: 0.3), overflow: TextOverflow.ellipsis, ), ), diff --git a/lib/pages/message/system/controller.dart b/lib/pages/message/system/controller.dart new file mode 100644 index 00000000..ad28af56 --- /dev/null +++ b/lib/pages/message/system/controller.dart @@ -0,0 +1,3 @@ +import 'package:get/get.dart'; + +class MessageSystemController extends GetxController {} diff --git a/lib/pages/message/system/index.dart b/lib/pages/message/system/index.dart new file mode 100644 index 00000000..70b9fc6d --- /dev/null +++ b/lib/pages/message/system/index.dart @@ -0,0 +1,4 @@ +library message_system; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/message/system/view.dart b/lib/pages/message/system/view.dart new file mode 100644 index 00000000..da0f1219 --- /dev/null +++ b/lib/pages/message/system/view.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class MessageSystemPage extends StatefulWidget { + const MessageSystemPage({super.key}); + + @override + State createState() => _MessageSystemPageState(); +} + +class _MessageSystemPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('系统通知'), + ), + ); + } +} diff --git a/lib/pages/whisper/controller.dart b/lib/pages/whisper/controller.dart index f3cc47d6..2614bf5a 100644 --- a/lib/pages/whisper/controller.dart +++ b/lib/pages/whisper/controller.dart @@ -18,19 +18,19 @@ class WhisperController extends GetxController { { 'icon': Icons.alternate_email, 'title': '@ 我的', - 'path': '', + 'path': '/messageAt', 'count': 0, }, { 'icon': Icons.thumb_up_outlined, 'title': '收到的赞', - 'path': '', + 'path': '/messageLike', 'count': 0, }, { 'icon': Icons.notifications_none_outlined, 'title': '系统通知', - 'path': '', + 'path': '/messageSystem', 'count': 0, } ].obs; diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index bb036c6e..a6b48f0d 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -4,7 +4,10 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/pages/follow_search/view.dart'; +import 'package:pilipala/pages/message/at/index.dart'; +import 'package:pilipala/pages/message/like/index.dart'; import 'package:pilipala/pages/message/reply/index.dart'; +import 'package:pilipala/pages/message/system/index.dart'; import 'package:pilipala/pages/setting/pages/logs.dart'; import '../pages/about/index.dart'; @@ -181,6 +184,13 @@ class Routes { name: '/actionMenuSet', page: () => const ActionMenuSetPage()), // 回复我的 CustomGetPage(name: '/messageReply', page: () => const MessageReplyPage()), + // @我的 + CustomGetPage(name: '/messageAt', page: () => const MessageAtPage()), + // 收到的赞 + CustomGetPage(name: '/messageLike', page: () => const MessageLikePage()), + // 系统通知 + CustomGetPage( + name: '/messageSystem', page: () => const MessageSystemPage()), ]; } From 2503d5cbb4833ad19d63eb71919b097d10052026 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sun, 16 Jun 2024 19:09:05 +0800 Subject: [PATCH 06/19] =?UTF-8?q?opt:=20=E6=90=9C=E7=B4=A2=E7=BB=93?= =?UTF-8?q?=E6=9E=9C=E4=B8=BA=E7=A9=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common/widgets/http_error.dart | 38 +++++++++------ lib/http/search.dart | 16 +++++-- lib/models/search/result.dart | 10 ++-- lib/pages/search_panel/controller.dart | 4 +- .../search_panel/widgets/video_panel.dart | 47 ++++++++++++------- 5 files changed, 72 insertions(+), 43 deletions(-) diff --git a/lib/common/widgets/http_error.dart b/lib/common/widgets/http_error.dart index cbc6659b..0381319e 100644 --- a/lib/common/widgets/http_error.dart +++ b/lib/common/widgets/http_error.dart @@ -2,12 +2,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; class HttpError extends StatelessWidget { - const HttpError( - {required this.errMsg, required this.fn, this.btnText, super.key}); + const HttpError({ + required this.errMsg, + required this.fn, + this.btnText, + this.isShowBtn = true, + super.key, + }); final String? errMsg; final Function()? fn; final String? btnText; + final bool isShowBtn; @override Widget build(BuildContext context) { @@ -29,20 +35,22 @@ class HttpError extends StatelessWidget { style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 20), - FilledButton.tonal( - onPressed: () { - fn!(); - }, - style: ButtonStyle( - backgroundColor: MaterialStateProperty.resolveWith((states) { - return Theme.of(context).colorScheme.primary.withAlpha(20); - }), + if (isShowBtn) + FilledButton.tonal( + onPressed: () { + fn!(); + }, + style: ButtonStyle( + backgroundColor: MaterialStateProperty.resolveWith((states) { + return Theme.of(context).colorScheme.primary.withAlpha(20); + }), + ), + child: Text( + btnText ?? '点击重试', + style: + TextStyle(color: Theme.of(context).colorScheme.primary), + ), ), - child: Text( - btnText ?? '点击重试', - style: TextStyle(color: Theme.of(context).colorScheme.primary), - ), - ), ], ), ), diff --git a/lib/http/search.dart b/lib/http/search.dart index 075defc7..403e6a37 100644 --- a/lib/http/search.dart +++ b/lib/http/search.dart @@ -88,7 +88,11 @@ class SearchHttp { if (tids != null && tids != -1) 'tids': tids, }; var res = await Request().get(Api.searchByType, data: reqData); - if (res.data['code'] == 0 && res.data['data']['numPages'] > 0) { + if (res.data['code'] == 0) { + if (res.data['data']['numPages'] == 0) { + // 我想返回数据,使得可以通过data.list 取值,结果为[] + return {'status': true, 'data': Data()}; + } Object data; try { switch (searchType) { @@ -125,9 +129,7 @@ class SearchHttp { return { 'status': false, 'data': [], - 'msg': res.data['data'] != null && res.data['data']['numPages'] == 0 - ? '没有相关数据' - : res.data['message'], + 'msg': res.data['message'], }; } } @@ -206,3 +208,9 @@ class SearchHttp { } } } + +class Data { + List list; + + Data({this.list = const []}); +} diff --git a/lib/models/search/result.dart b/lib/models/search/result.dart index 81917b72..b903c873 100644 --- a/lib/models/search/result.dart +++ b/lib/models/search/result.dart @@ -5,10 +5,12 @@ class SearchVideoModel { SearchVideoModel({this.list}); List? list; SearchVideoModel.fromJson(Map json) { - list = json['result'] - .where((e) => e['available'] == true) - .map((e) => SearchVideoItemModel.fromJson(e)) - .toList(); + list = json['result'] == null + ? [] + : json['result'] + .where((e) => e['available'] == true) + .map((e) => SearchVideoItemModel.fromJson(e)) + .toList(); } } diff --git a/lib/pages/search_panel/controller.dart b/lib/pages/search_panel/controller.dart index 35113198..dc0b2bac 100644 --- a/lib/pages/search_panel/controller.dart +++ b/lib/pages/search_panel/controller.dart @@ -30,9 +30,9 @@ class SearchPanelController extends GetxController { ); if (result['status']) { if (type == 'onRefresh') { - resultList.value = result['data'].list; + resultList.value = result['data'].list ?? []; } else { - resultList.addAll(result['data'].list); + resultList.addAll(result['data'].list ?? []); } page.value++; onPushDetail(keyword, resultList); diff --git a/lib/pages/search_panel/widgets/video_panel.dart b/lib/pages/search_panel/widgets/video_panel.dart index 15745bde..f43e2eec 100644 --- a/lib/pages/search_panel/widgets/video_panel.dart +++ b/lib/pages/search_panel/widgets/video_panel.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/video_card_h.dart'; import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/pages/search/widgets/search_text.dart'; @@ -25,25 +26,35 @@ class SearchVideoPanel extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.only(top: 36), - child: ListView.builder( - controller: ctr!.scrollController, - addAutomaticKeepAlives: false, - addRepaintBoundaries: false, - itemCount: list!.length, - itemBuilder: (context, index) { - var i = list![index]; - return Padding( - padding: index == 0 - ? const EdgeInsets.only(top: 2) - : EdgeInsets.zero, - child: VideoCardH( - videoItem: i, - showPubdate: true, - source: 'search', + child: list!.isNotEmpty + ? ListView.builder( + controller: ctr!.scrollController, + addAutomaticKeepAlives: false, + addRepaintBoundaries: false, + itemCount: list!.length, + itemBuilder: (context, index) { + var i = list![index]; + return Padding( + padding: index == 0 + ? const EdgeInsets.only(top: 2) + : EdgeInsets.zero, + child: VideoCardH( + videoItem: i, + showPubdate: true, + source: 'search', + ), + ); + }, + ) + : CustomScrollView( + slivers: [ + HttpError( + errMsg: '没有数据', + isShowBtn: false, + fn: () => {}, + ) + ], ), - ); - }, - ), ), // 分类筛选 Container( From 75525595c6258a9561d1b88845babb626aaf07ad Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sun, 16 Jun 2024 20:59:55 +0800 Subject: [PATCH 07/19] =?UTF-8?q?opt:=20=E9=A6=96=E9=A1=B5tab=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/home/view.dart | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/pages/home/view.dart b/lib/pages/home/view.dart index a25389bd..e485fe41 100644 --- a/lib/pages/home/view.dart +++ b/lib/pages/home/view.dart @@ -357,25 +357,29 @@ class CustomChip extends StatelessWidget { Widget build(BuildContext context) { final ColorScheme colorTheme = Theme.of(context).colorScheme; final Color secondaryContainer = colorTheme.secondaryContainer; + final Color onPrimary = colorTheme.onPrimary; + final Color primary = colorTheme.primary; final TextStyle chipTextStyle = selected - ? const TextStyle(fontWeight: FontWeight.bold, fontSize: 13) - : const TextStyle(fontSize: 13); - final ColorScheme colorScheme = Theme.of(context).colorScheme; + ? TextStyle(fontSize: 13, color: onPrimary) + : TextStyle(fontSize: 13, color: colorTheme.onSecondaryContainer); const VisualDensity visualDensity = VisualDensity(horizontal: -4.0, vertical: -2.0); return InputChip( - side: BorderSide( - color: selected - ? colorScheme.onSecondaryContainer.withOpacity(0.2) - : Colors.transparent, - ), + side: BorderSide.none, backgroundColor: secondaryContainer, - selectedColor: secondaryContainer, - color: MaterialStateProperty.resolveWith( - (Set states) => secondaryContainer.withAlpha(200)), - padding: const EdgeInsets.fromLTRB(7, 1, 7, 1), + color: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.selected) || + states.contains(MaterialState.hovered)) { + return primary; + } + return colorTheme.secondaryContainer; + }), + padding: const EdgeInsets.fromLTRB(6, 1, 6, 1), label: Text(label, style: chipTextStyle), onPressed: () => onTap(), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), selected: selected, showCheckmark: false, visualDensity: visualDensity, From 56c063538964c5119c10572b8e606118a87ea31b Mon Sep 17 00:00:00 2001 From: guozhigq Date: Wed, 19 Jun 2024 23:39:19 +0800 Subject: [PATCH 08/19] =?UTF-8?q?mod:=20scheme=20=E8=A1=A5=E5=85=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/utils/app_scheme.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/utils/app_scheme.dart b/lib/utils/app_scheme.dart index a83b7809..02c0084f 100644 --- a/lib/utils/app_scheme.dart +++ b/lib/utils/app_scheme.dart @@ -19,6 +19,7 @@ class PiliSchame { /// 完整链接进入 b23.无效 appScheme.getLatestScheme().then((SchemeEntity? value) { + print('getLatestScheme value: $value'); if (value != null) { _routePush(value); } @@ -26,6 +27,8 @@ class PiliSchame { /// 注册从外部打开的Scheme监听信息 # appScheme.registerSchemeListener().listen((SchemeEntity? event) { + print('registerSchemeListener event: $event'); + if (event != null) { _routePush(event); } @@ -92,6 +95,12 @@ class PiliSchame { 'id': 'cv$id', 'dynamicType': 'read' }); + } else if (host == 'pgc') { + if (path.contains('ep')) { + final String lastPathSegment = path.split('/').last; + RoutePush.bangumiPush( + null, int.parse(lastPathSegment.split('?').first)); + } } } if (scheme == 'https') { From 4de15ad6b3282b6b2e2354509e92bc6648d93dbe Mon Sep 17 00:00:00 2001 From: guozhigq Date: Thu, 20 Jun 2024 00:06:45 +0800 Subject: [PATCH 09/19] =?UTF-8?q?opt:=20=20=5FroutePush=20=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E5=88=86=E6=94=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/utils/app_scheme.dart | 138 +++++++++++++++++++++----------------- 1 file changed, 76 insertions(+), 62 deletions(-) diff --git a/lib/utils/app_scheme.dart b/lib/utils/app_scheme.dart index 39a36cad..17d20bcd 100644 --- a/lib/utils/app_scheme.dart +++ b/lib/utils/app_scheme.dart @@ -1,5 +1,6 @@ import 'package:appscheme/appscheme.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:pilipala/utils/route_push.dart'; @@ -19,7 +20,6 @@ class PiliSchame { /// 完整链接进入 b23.无效 appScheme.getLatestScheme().then((SchemeEntity? value) { - print('getLatestScheme value: $value'); if (value != null) { _routePush(value); } @@ -27,8 +27,6 @@ class PiliSchame { /// 注册从外部打开的Scheme监听信息 # appScheme.registerSchemeListener().listen((SchemeEntity? event) { - print('registerSchemeListener event: $event'); - if (event != null) { _routePush(event); } @@ -41,66 +39,82 @@ class PiliSchame { final String host = value.host; final String path = value.path; if (scheme == 'bilibili') { - if (host == 'root') { - Navigator.popUntil( - Get.context!, (Route route) => route.isFirst); - } else if (host == 'space') { - final String mid = path.split('/').last; - Get.toNamed( - '/member?mid=$mid', - arguments: {'face': null}, - ); - } else if (host == 'video') { - String pathQuery = path.split('/').last; - final numericRegex = RegExp(r'^[0-9]+$'); - if (numericRegex.hasMatch(pathQuery)) { - pathQuery = 'AV$pathQuery'; - } - Map map = IdUtils.matchAvorBv(input: pathQuery); - if (map.containsKey('AV')) { - _videoPush(map['AV'], null); - } else if (map.containsKey('BV')) { - _videoPush(null, map['BV']); - } else { - SmartDialog.showToast('投稿匹配失败'); - } - } else if (host == 'live') { - final String roomId = path.split('/').last; - Get.toNamed('/liveRoom?roomid=$roomId', - arguments: {'liveItem': null, 'heroTag': roomId}); - } else if (host == 'bangumi') { - if (path.startsWith('/season')) { - final String seasonId = path.split('/').last; - RoutePush.bangumiPush(int.parse(seasonId), null); - } - } else if (host == 'opus') { - if (path.startsWith('/detail')) { - var opusId = path.split('/').last; - Get.toNamed( - '/webview', - parameters: { - 'url': 'https://www.bilibili.com/opus/$opusId', - 'type': 'url', - 'pageTitle': '', - }, + switch (host) { + case 'root': + Navigator.popUntil( + Get.context!, (Route route) => route.isFirst); + break; + case 'space': + final String mid = path.split('/').last; + Get.toNamed( + '/member?mid=$mid', + arguments: {'face': null}, ); - } - } else if (host == 'search') { - Get.toNamed('/searchResult', parameters: {'keyword': ''}); - } else if (host == 'article') { - final String id = path.split('/').last.split('?').first; - Get.toNamed('/htmlRender', parameters: { - 'url': 'https://www.bilibili.com/read/cv$id', - 'title': 'cv$id', - 'id': 'cv$id', - 'dynamicType': 'read' - }); - } else if (host == 'pgc') { - if (path.contains('ep')) { - final String lastPathSegment = path.split('/').last; - RoutePush.bangumiPush( - null, int.parse(lastPathSegment.split('?').first)); - } + break; + case 'video': + String pathQuery = path.split('/').last; + final numericRegex = RegExp(r'^[0-9]+$'); + if (numericRegex.hasMatch(pathQuery)) { + pathQuery = 'AV$pathQuery'; + } + Map map = IdUtils.matchAvorBv(input: pathQuery); + if (map.containsKey('AV')) { + _videoPush(map['AV'], null); + } else if (map.containsKey('BV')) { + _videoPush(null, map['BV']); + } else { + SmartDialog.showToast('投稿匹配失败'); + } + break; + case 'live': + final String roomId = path.split('/').last; + Get.toNamed( + '/liveRoom?roomid=$roomId', + arguments: {'liveItem': null, 'heroTag': roomId}, + ); + break; + case 'bangumi': + if (path.startsWith('/season')) { + final String seasonId = path.split('/').last; + RoutePush.bangumiPush(int.parse(seasonId), null); + } + break; + case 'opus': + if (path.startsWith('/detail')) { + var opusId = path.split('/').last; + Get.toNamed( + '/webview', + parameters: { + 'url': 'https://www.bilibili.com/opus/$opusId', + 'type': 'url', + 'pageTitle': '', + }, + ); + } + break; + case 'search': + Get.toNamed('/searchResult', parameters: {'keyword': ''}); + break; + case 'article': + final String id = path.split('/').last.split('?').first; + Get.toNamed('/htmlRender', parameters: { + 'url': 'https://www.bilibili.com/read/cv$id', + 'title': 'cv$id', + 'id': 'cv$id', + 'dynamicType': 'read' + }); + break; + case 'pgc': + if (path.contains('ep')) { + final String lastPathSegment = path.split('/').last; + RoutePush.bangumiPush( + null, int.parse(lastPathSegment.split('?').first)); + } + break; + default: + SmartDialog.showToast('未匹配地址,请联系开发者'); + Clipboard.setData(ClipboardData(text: value.toJson().toString())); + break; } } if (scheme == 'https') { From 1db1d8f598ac4b7236f2abe8e9fc7eab3074602c Mon Sep 17 00:00:00 2001 From: guozhigq Date: Thu, 20 Jun 2024 23:38:49 +0800 Subject: [PATCH 10/19] =?UTF-8?q?feat:=20=E7=82=B9=E5=87=BBtab=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E9=A1=B6=E9=83=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/video/detail/controller.dart | 12 ++++++++++++ lib/pages/video/detail/reply/view.dart | 3 +++ lib/pages/video/detail/view.dart | 2 ++ 3 files changed, 17 insertions(+) diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index 38c62d7e..ea85a5b9 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -109,6 +109,7 @@ class VideoDetailController extends GetxController ].obs; RxDouble sheetHeight = 0.0.obs; RxString archiveSourceType = 'dash'.obs; + ScrollController? replyScrillController; @override void onInit() { @@ -551,4 +552,15 @@ class VideoDetailController extends GetxController cover.value = videoItem['pic'] = pic; } } + + void onControllerCreated(ScrollController controller) { + replyScrillController = controller; + } + + void onTapTabbar(int index) { + if (index == 1 && tabCtr.index == 1) { + replyScrillController?.animateTo(0, + duration: const Duration(milliseconds: 300), curve: Curves.ease); + } + } } diff --git a/lib/pages/video/detail/reply/view.dart b/lib/pages/video/detail/reply/view.dart index 653fe7e0..be1bd331 100644 --- a/lib/pages/video/detail/reply/view.dart +++ b/lib/pages/video/detail/reply/view.dart @@ -19,12 +19,14 @@ class VideoReplyPanel extends StatefulWidget { final int? oid; final int rpid; final String? replyLevel; + final Function(ScrollController)? onControllerCreated; const VideoReplyPanel({ this.bvid, this.oid, this.rpid = 0, this.replyLevel, + this.onControllerCreated, super.key, }); @@ -68,6 +70,7 @@ class _VideoReplyPanelState extends State _futureBuilderFuture = _videoReplyController.queryReplyList(); scrollController = ScrollController(); + widget.onControllerCreated?.call(scrollController); fabAnimationCtr.forward(); scrollListener(); } diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 22271c2b..ea29bf78 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -387,6 +387,7 @@ class _VideoDetailPageState extends State dividerColor: Colors.transparent, tabs: vdCtr.tabs.map((String name) => Tab(text: name)).toList(), + onTap: (index) => vdCtr.onTapTabbar(index), ), ), ), @@ -676,6 +677,7 @@ class _VideoDetailPageState extends State () => VideoReplyPanel( bvid: vdCtr.bvid, oid: vdCtr.oid.value, + onControllerCreated: vdCtr.onControllerCreated, ), ) ], From c5247b27c7b478bd15912d18521f1f13448a078b Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sun, 23 Jun 2024 23:12:04 +0800 Subject: [PATCH 11/19] =?UTF-8?q?mod:=20=E6=B6=88=E6=81=AF=E8=AE=A1?= =?UTF-8?q?=E6=95=B0=E6=B8=85=E9=9B=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/whisper/view.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/pages/whisper/view.dart b/lib/pages/whisper/view.dart index fccdd844..e31e942e 100644 --- a/lib/pages/whisper/view.dart +++ b/lib/pages/whisper/view.dart @@ -69,7 +69,14 @@ class _WhisperPageState extends State { children: [ ..._whisperController.noticesList.map((element) { return InkWell( - onTap: () => Get.toNamed(element['path']), + onTap: () { + Get.toNamed(element['path']); + + if (element['count'] > 0) { + element['count'] = 0; + } + _whisperController.noticesList.refresh(); + }, onLongPress: () {}, borderRadius: StyleString.mdRadius, child: Column( From 4fa9bdc3804c4ca3f4bd2e33973b42d7969c532a Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sun, 23 Jun 2024 23:23:05 +0800 Subject: [PATCH 12/19] =?UTF-8?q?mod:=20chatItem=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../whisper_detail/widget/chat_item.dart | 90 ++++++++----------- 1 file changed, 39 insertions(+), 51 deletions(-) diff --git a/lib/pages/whisper_detail/widget/chat_item.dart b/lib/pages/whisper_detail/widget/chat_item.dart index f64cf223..77e38073 100644 --- a/lib/pages/whisper_detail/widget/chat_item.dart +++ b/lib/pages/whisper_detail/widget/chat_item.dart @@ -1,4 +1,5 @@ // ignore_for_file: must_be_immutable +// ignore_for_file: constant_identifier_names import 'dart:convert'; import 'package:flutter/material.dart'; @@ -8,7 +9,6 @@ import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/utils/route_push.dart'; import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/storage.dart'; - import '../../../http/search.dart'; enum MsgType { @@ -409,12 +409,6 @@ class ChatItem extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ const SizedBox(width: safeDistanceval), - if (isOwner) - Text( - Utils.dateFormat(item.timestamp), - style: Theme.of(context).textTheme.labelSmall!.copyWith( - color: Theme.of(context).colorScheme.outline), - ), Container( constraints: const BoxConstraints( maxWidth: 300.0, // 设置最大宽度为200.0 @@ -444,51 +438,45 @@ class ChatItem extends StatelessWidget { right: 8, ), padding: const EdgeInsets.all(paddingVal), - child: messageContent(context), - // child: Column( - // crossAxisAlignment: isOwner - // ? CrossAxisAlignment.end - // : CrossAxisAlignment.start, - // children: [ - // messageContent(context), - // SizedBox(height: isPic ? 7 : 2), - // Row( - // mainAxisSize: MainAxisSize.min, - // children: [ - // Text( - // Utils.dateFormat(item.timestamp), - // style: Theme.of(context) - // .textTheme - // .labelSmall! - // .copyWith( - // color: isOwner - // ? Theme.of(context) - // .colorScheme - // .onPrimary - // .withOpacity(0.8) - // : Theme.of(context) - // .colorScheme - // .onSecondaryContainer - // .withOpacity(0.8)), - // ), - // item.msgStatus == 1 - // ? Text( - // ' 已撤回', - // style: - // Theme.of(context).textTheme.labelSmall!, - // ) - // : const SizedBox() - // ], - // ) - // ], - // ), - ), - if (!isOwner) - Text( - Utils.dateFormat(item.timestamp), - style: Theme.of(context).textTheme.labelSmall!.copyWith( - color: Theme.of(context).colorScheme.outline), + child: Column( + crossAxisAlignment: isOwner + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + messageContent(context), + SizedBox(height: isPic ? 7 : 4), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + Utils.dateFormat(item.timestamp), + style: Theme.of(context) + .textTheme + .labelSmall! + .copyWith( + color: isOwner + ? Theme.of(context) + .colorScheme + .onPrimary + .withOpacity(0.8) + : Theme.of(context) + .colorScheme + .onSecondaryContainer + .withOpacity(0.8)), + ), + item.msgStatus == 1 + ? Text( + ' 已撤回', + style: Theme.of(context) + .textTheme + .labelSmall!, + ) + : const SizedBox() + ], + ) + ], ), + ), const SizedBox(width: safeDistanceval), ], ), From cc32224daf5c2304999d1dfe0952b6b41395c924 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Mon, 24 Jun 2024 23:43:37 +0800 Subject: [PATCH 13/19] =?UTF-8?q?feat:=20=E9=9F=B3=E9=A2=91=E8=BE=93?= =?UTF-8?q?=E5=87=BA=E6=96=B9=E5=BC=8F=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/models/video/play/ao_output.dart | 6 ++++++ lib/pages/setting/play_setting.dart | 29 ++++++++++++++++++++++++++++ lib/plugin/pl_player/controller.dart | 9 ++++++++- lib/utils/storage.dart | 2 ++ 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 lib/models/video/play/ao_output.dart diff --git a/lib/models/video/play/ao_output.dart b/lib/models/video/play/ao_output.dart new file mode 100644 index 00000000..170a78c5 --- /dev/null +++ b/lib/models/video/play/ao_output.dart @@ -0,0 +1,6 @@ +final List aoOutputList = [ + {'title': 'audiotrack,opensles', 'value': '0'}, + {'title': 'opensles,audiotrack', 'value': '1'}, + {'title': 'audiotrack', 'value': '2'}, + {'title': 'opensles', 'value': '3'}, +]; diff --git a/lib/pages/setting/play_setting.dart b/lib/pages/setting/play_setting.dart index 4a8495e5..0f7dcdc3 100644 --- a/lib/pages/setting/play_setting.dart +++ b/lib/pages/setting/play_setting.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; +import 'package:pilipala/models/video/play/ao_output.dart'; import 'package:pilipala/models/video/play/quality.dart'; import 'package:pilipala/pages/setting/widgets/select_dialog.dart'; import 'package:pilipala/plugin/pl_player/index.dart'; @@ -28,6 +29,7 @@ class _PlaySettingState extends State { late dynamic defaultDecode; late int defaultFullScreenMode; late int defaultBtmProgressBehavior; + late String defaultAoOutput; @override void initState() { @@ -44,6 +46,8 @@ class _PlaySettingState extends State { defaultValue: FullScreenMode.values.first.code); defaultBtmProgressBehavior = setting.get(SettingBoxKey.btmProgressBehavior, defaultValue: BtmProgresBehavior.values.first.code); + defaultAoOutput = + setting.get(SettingBoxKey.defaultAoOutput, defaultValue: '0'); } @override @@ -263,6 +267,31 @@ class _PlaySettingState extends State { } }, ), + ListTile( + dense: false, + title: Text('音频输出方式', style: titleStyle), + subtitle: Text( + '当前输出方式 ${aoOutputList.firstWhere((element) => element['value'] == defaultAoOutput)['title']}', + style: subTitleStyle, + ), + onTap: () async { + String? result = await showDialog( + context: context, + builder: (context) { + return SelectDialog( + title: '音频输出方式', + value: defaultAoOutput, + values: aoOutputList, + ); + }, + ); + if (result != null) { + defaultAoOutput = result; + setting.put(SettingBoxKey.defaultAoOutput, result); + setState(() {}); + } + }, + ), ListTile( dense: false, title: Text('默认全屏方式', style: titleStyle), diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 2865a117..a614d75d 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -13,6 +13,7 @@ import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; import 'package:ns_danmaku/ns_danmaku.dart'; import 'package:pilipala/http/video.dart'; +import 'package:pilipala/models/video/play/ao_output.dart'; import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/models/play_repeat.dart'; import 'package:pilipala/services/service_locator.dart'; @@ -453,7 +454,13 @@ class PlPlayerController { // 音量不一致 if (Platform.isAndroid) { await pp.setProperty("volume-max", "100"); - await pp.setProperty("ao", "audiotrack,opensles"); + String defaultAoOutput = + setting.get(SettingBoxKey.defaultAoOutput, defaultValue: '0'); + await pp.setProperty( + "ao", + aoOutputList + .where((e) => e['value'] == defaultAoOutput) + .first['title']); } await player.setAudioTrack( diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 4a163446..bf9074e3 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -102,6 +102,8 @@ class SettingBoxKey { autoPiP = 'autoPiP', enableAutoLongPressSpeed = 'enableAutoLongPressSpeed', enablePlayerControlAnimation = 'enablePlayerControlAnimation', + // 默认音频输出方式 + defaultAoOutput = 'defaultAoOutput', // youtube 双击快进快退 enableQuickDouble = 'enableQuickDouble', From 7aa02be25197df858b32c5bfe7eb3fbc8f8dd1f8 Mon Sep 17 00:00:00 2001 From: guozhibin Date: Tue, 25 Jun 2024 23:51:32 +0800 Subject: [PATCH 14/19] mod --- assets/images/coin.png | Bin 7159 -> 1701 bytes lib/pages/message/reply/view.dart | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/images/coin.png b/assets/images/coin.png index bc2952a7a5cf519a331b83c0f268eef88769d471..afca87b286cdc1f677aef1cf5c80f5d4fc4d980e 100644 GIT binary patch literal 1701 zcmV;W23q-vP)JA z04>JA0Xnmvc>n+gS4l)cRCt{2n|p}WMHI(BdpC6}ZBf&-4K=OYinQEHi?X*;TU1Cz z_9C*9!U&_VEdMF!K}3J_pe1^1DN$`pN*ku;J0GpRU-Zz3-nwmB`;G(W!ms1}?(cVZ z=H9)pc(i}+b-Z+ zU_G!1xC595RPrCi!N5FV1JDk1_pr4BtALwbOBbg*G|sW7VkHKppUL!otY^EddVf z4a3XSn3f#Mi9@>q80j(3XkaezUXI214Y-sdVjtkS7-c$umw=iAEci)4Lym=b92h`> zF*q1LcoHZ`E`y--YgNOHdM)y)o+#FFkUDFo^c3&HITz83v_h$29`PWe9~9& z)efPJfs8*3_!`&_oEK=Pu5jpkMNb8eQU@0m_EErX|7Aq(_e!OGAveIMM(15>OWt_h z2olnBIN`i_i{TIb0?9rS_|xbvZ-~4w%|SSXqK3j4bA`#{>5zc(Z*&}m>1hXN^vxa} zygI-Z6=yd?iInTLC=0$Bm>FQMa@9F1e{))gJKYQvWsmh#Xt3i*^s=WS`|0X;=h;38Kx>y6nQtWx7^My=x-BPrD)EBDxK2H0!ZDuCaV%c(oO_ay zC$^2tHQ-h=V3Vf;$2**b%x;p5rm548I;u9a{SU$q_5pQfiZLgcF%m8I!2u>PE5|_oZ4aF7a6&%FXtX9ev4p{r zuov)mWL$fIYjKmY?Lt?lI~@@gAIP2igV|AO<9Ayl<37V54;kC;b#;2tbh<9U#4C-= z+ft4@LW8Z#0!5iFOGAw}nog$#nD`bWQ|Rv*6d%W>Io>-=C*kY95Xbtnk=w4vYqGv9 z3m(W+rx(4Be|2%^W^-PJ!G*vjIU>28nj74%r04PwQJk27NvoOnU+AxjWIMD4R%C7- z>9TJE1xTzzwgJ?lE=q^QjVEp^pFqZiYGvZQAQLo(PncGLddUWD6G`BP7WO!;D>W1H{_3!Avd% z4nC-JoM&NWaS&Skjv2GiQ-@hbo)lZeVxH+Nu{s}2anRtk`r5NN6nNXg2Tu3Y!7ixy zx}4>0QOeq-fkhuIav0U2i(98*v(+)AsNOSqI>CGZvF)&=L5)TUi4*vsQny`qo=w7O z`+Ls4(8!eJX(DPJU&}WrF(gGbS+p$=zyK$j>r~qLX$EFYNUB@Urf(i!1;I$*3&$i|ls)zq zsx`RbB2mGsMMuXh!cNV+6th$|qnWFg7~>1}>yj!5Ns3-I@El|L#{G6{ zv-yCewpBAtC53WjbZdI>E3h=JL(AN1OYvuOu6o_Of%iRsL)f9u=lMzHZKaGV-5K|5 vP(+T3@c-z-$lt%{Lq-xQ_W&0Z@=E&$KxEtUmHWF=00000NkvXXu0mjfN2xS6 literal 7159 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 diff --git a/lib/pages/message/reply/view.dart b/lib/pages/message/reply/view.dart index 881f8650..9776c7f7 100644 --- a/lib/pages/message/reply/view.dart +++ b/lib/pages/message/reply/view.dart @@ -196,7 +196,7 @@ class ReplyItem extends StatelessWidget { style: TextStyle(color: outline), ), const SizedBox(width: 16), - Text('回复', style: TextStyle(color: outline)), + // Text('回复', style: TextStyle(color: outline)), ], ) ], From 0031b229021429318174eed48303312837c89499 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Wed, 26 Jun 2024 22:18:49 +0800 Subject: [PATCH 15/19] =?UTF-8?q?mod:=20=E6=9C=AA=E5=BC=80=E5=8F=91?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=85=B3=E9=97=AD=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/whisper/controller.dart | 2 +- lib/pages/whisper/view.dart | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/pages/whisper/controller.dart b/lib/pages/whisper/controller.dart index 2614bf5a..749a3482 100644 --- a/lib/pages/whisper/controller.dart +++ b/lib/pages/whisper/controller.dart @@ -17,7 +17,7 @@ class WhisperController extends GetxController { }, { 'icon': Icons.alternate_email, - 'title': '@ 我的', + 'title': '@我的', 'path': '/messageAt', 'count': 0, }, diff --git a/lib/pages/whisper/view.dart b/lib/pages/whisper/view.dart index e31e942e..9436e2be 100644 --- a/lib/pages/whisper/view.dart +++ b/lib/pages/whisper/view.dart @@ -1,5 +1,6 @@ import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/skeleton/skeleton.dart'; @@ -70,6 +71,11 @@ class _WhisperPageState extends State { ..._whisperController.noticesList.map((element) { return InkWell( onTap: () { + if (['/messageAt', '/messageSystem'] + .contains(element['path'])) { + SmartDialog.showToast('功能开发中'); + return; + } Get.toNamed(element['path']); if (element['count'] > 0) { From dbfc31a1dff498d11d4e76f8fc3ec1f5f88ea728 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Wed, 26 Jun 2024 22:29:11 +0800 Subject: [PATCH 16/19] =?UTF-8?q?v1.0.24=20=E6=9B=B4=E6=96=B0=E6=97=A5?= =?UTF-8?q?=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- change_log/1.0.24.0626.md | 23 +++++++++++++++++++++++ pubspec.yaml | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 change_log/1.0.24.0626.md diff --git a/change_log/1.0.24.0626.md b/change_log/1.0.24.0626.md new file mode 100644 index 00000000..d9a8892f --- /dev/null +++ b/change_log/1.0.24.0626.md @@ -0,0 +1,23 @@ +## 1.0.24 + +### 功能 ++ 私信功能 ++ 回复我的、收到的赞查看 ++ 新的登录方式 ++ 全屏选集 ++ 一键三连 ++ 按分区搜索 + +### 优化 ++ 页面跳转动画 ++ 评论区跳转 + +### 修复 ++ 音画不同步问题 ++ 分集字幕未同步 ++ 多语言字幕 ++ 弹幕设置未生效 ++ + + +问题反馈、功能建议请查看「关于」页面。 diff --git a/pubspec.yaml b/pubspec.yaml index 66187abf..09169473 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.23+1023 +version: 1.0.24+1024 environment: sdk: ">=3.0.0 <4.0.0" From 7301673783dfde6c6ef6114a77ff57cc4db8e782 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Fri, 28 Jun 2024 23:24:42 +0800 Subject: [PATCH 17/19] =?UTF-8?q?mod:=20=E8=A7=86=E9=A2=91=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E5=8C=BA=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/video/detail/introduction/view.dart | 21 +++++++++++-------- .../introduction/widgets/action_item.dart | 1 + 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index 93cc26c9..1e8d97f1 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -2,6 +2,7 @@ import 'package:easy_debounce/easy_throttle.dart'; import 'package:expandable/expandable.dart'; import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; @@ -629,11 +630,12 @@ class _VideoInfoState extends State with TickerProviderStateMixin { child: Icon( key: ValueKey(likeStatus), likeStatus - ? Icons.thumb_up - : Icons.thumb_up_alt_outlined, + ? FontAwesomeIcons.solidThumbsUp + : FontAwesomeIcons.thumbsUp, color: likeStatus ? colorScheme.primary : colorScheme.outline, + size: 21, ), ), const SizedBox(height: 6), @@ -663,7 +665,8 @@ class _VideoInfoState extends State with TickerProviderStateMixin { (const IconThemeData.fallback().size! + 5) / 2, child: progressWidget(_progress)), ActionItem( - icon: Image.asset('assets/images/coin.png', width: 30), + icon: const Icon(FontAwesomeIcons.b), + selectIcon: const Icon(FontAwesomeIcons.b), onTap: handleState(videoIntroController.actionCoinVideo), selectStatus: videoIntroController.hasCoin.value, text: widget.videoDetail!.stat!.coin!.toString(), @@ -681,8 +684,8 @@ class _VideoInfoState extends State with TickerProviderStateMixin { (const IconThemeData.fallback().size! + 5) / 2, child: progressWidget(_progress)), ActionItem( - icon: const Icon(Icons.star_border), - selectIcon: const Icon(Icons.star), + icon: const Icon(FontAwesomeIcons.star), + selectIcon: const Icon(FontAwesomeIcons.solidStar), onTap: () => showFavBottomSheet(), onLongPress: () => showFavBottomSheet(type: 'longPress'), selectStatus: videoIntroController.hasFav.value, @@ -692,7 +695,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ), ), 'watchLater': ActionItem( - icon: const Icon(Icons.watch_later_outlined), + icon: const Icon(FontAwesomeIcons.clock), onTap: () async { final res = await UserHttp.toViewLater(bvid: widget.videoDetail!.bvid); @@ -702,15 +705,15 @@ class _VideoInfoState extends State with TickerProviderStateMixin { text: '稍后看', ), 'share': ActionItem( - icon: const Icon(Icons.share), + icon: const Icon(FontAwesomeIcons.shareFromSquare), onTap: () => videoIntroController.actionShareVideo(), selectStatus: false, text: '分享', ), 'dislike': Obx( () => ActionItem( - icon: const Icon(Icons.thumb_down_alt_outlined), - selectIcon: const Icon(Icons.thumb_down), + icon: const Icon(FontAwesomeIcons.thumbsDown), + selectIcon: const Icon(FontAwesomeIcons.solidThumbsDown), onTap: () {}, selectStatus: videoIntroController.hasDisLike.value, text: '不喜欢', diff --git a/lib/pages/video/detail/introduction/widgets/action_item.dart b/lib/pages/video/detail/introduction/widgets/action_item.dart index 3288b2fa..fd0b9fef 100644 --- a/lib/pages/video/detail/introduction/widgets/action_item.dart +++ b/lib/pages/video/detail/introduction/widgets/action_item.dart @@ -51,6 +51,7 @@ class ActionItem extends StatelessWidget { color: selectStatus ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.outline, + size: 20, ) : Image.asset( key: ValueKey(selectStatus), From a63a9a28d7df50d38a359373fc9dcd2a680a4e3f Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sat, 29 Jun 2024 13:53:43 +0800 Subject: [PATCH 18/19] =?UTF-8?q?feat:=20=E6=9C=80=E8=BF=91=E7=82=B9?= =?UTF-8?q?=E8=B5=9E=E7=9A=84=E8=A7=86=E9=A2=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/http/member.dart | 5 +- lib/models/member/like.dart | 210 ++++++++++++++++++++++++ lib/pages/member/controller.dart | 10 ++ lib/pages/member/view.dart | 62 +++++-- lib/pages/member/widgets/conis.dart | 8 +- lib/pages/member/widgets/like.dart | 31 ++++ lib/pages/member_like/widgets/item.dart | 96 +++++++++++ 7 files changed, 400 insertions(+), 22 deletions(-) create mode 100644 lib/models/member/like.dart create mode 100644 lib/pages/member/widgets/like.dart create mode 100644 lib/pages/member_like/widgets/item.dart diff --git a/lib/http/member.dart b/lib/http/member.dart index 1af0f9a4..20a2c728 100644 --- a/lib/http/member.dart +++ b/lib/http/member.dart @@ -1,5 +1,6 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:hive/hive.dart'; +import 'package:pilipala/models/member/like.dart'; import '../common/constants.dart'; import '../models/dynamics/result.dart'; import '../models/follow/result.dart'; @@ -328,7 +329,9 @@ class MemberHttp { if (res.data['code'] == 0) { return { 'status': true, - 'data': MemberSeasonsDataModel.fromJson(res.data['data']['items_lists']) + 'data': res.data['data']['list'] + .map((e) => MemberLikeDataModel.fromJson(e)) + .toList(), }; } else { return { diff --git a/lib/models/member/like.dart b/lib/models/member/like.dart new file mode 100644 index 00000000..df71e2ec --- /dev/null +++ b/lib/models/member/like.dart @@ -0,0 +1,210 @@ +class MemberLikeDataModel { + MemberLikeDataModel({ + this.aid, + this.videos, + this.tid, + this.tname, + this.pic, + this.title, + this.pubdate, + this.ctime, + this.desc, + this.state, + this.duration, + this.redirectUrl, + this.rights, + this.owner, + this.stat, + this.dimension, + this.cover43, + this.bvid, + this.interVideo, + this.resourceType, + this.subtitle, + this.enableVt, + }); + + final int? aid; + final int? videos; + final int? tid; + final String? tname; + final String? pic; + final String? title; + final int? pubdate; + final int? ctime; + final String? desc; + final int? state; + final int? duration; + final String? redirectUrl; + final Rights? rights; + final Owner? owner; + final Stat? stat; + final Dimension? dimension; + final String? cover43; + final String? bvid; + final bool? interVideo; + final String? resourceType; + final String? subtitle; + final int? enableVt; + + factory MemberLikeDataModel.fromJson(Map json) => + MemberLikeDataModel( + aid: json["aid"], + videos: json["videos"], + tid: json["tid"], + tname: json["tname"], + pic: json["pic"], + title: json["title"], + pubdate: json["pubdate"], + ctime: json["ctime"], + desc: json["desc"], + state: json["state"], + duration: json["duration"], + redirectUrl: json["redirect_url"], + rights: Rights.fromJson(json["rights"]), + owner: Owner.fromJson(json["owner"]), + stat: Stat.fromJson(json["stat"]), + dimension: Dimension.fromJson(json["dimension"]), + cover43: json["cover43"], + bvid: json["bvid"], + interVideo: json["inter_video"], + resourceType: json["resource_type"], + subtitle: json["subtitle"], + enableVt: json["enable_vt"], + ); +} + +class Dimension { + Dimension({ + required this.width, + required this.height, + required this.rotate, + }); + + final int width; + final int height; + final int rotate; + + factory Dimension.fromJson(Map json) => Dimension( + width: json["width"], + height: json["height"], + rotate: json["rotate"], + ); +} + +class Owner { + Owner({ + required this.mid, + required this.name, + required this.face, + }); + + final int mid; + final String name; + final String face; + + factory Owner.fromJson(Map json) => Owner( + mid: json["mid"], + name: json["name"], + face: json["face"], + ); +} + +class Rights { + Rights({ + required this.bp, + required this.elec, + required this.download, + required this.movie, + required this.pay, + required this.hd5, + required this.noReprint, + required this.autoplay, + required this.ugcPay, + required this.isCooperation, + required this.ugcPayPreview, + required this.noBackground, + required this.arcPay, + required this.payFreeWatch, + }); + + final int bp; + final int elec; + final int download; + final int movie; + final int pay; + final int hd5; + final int noReprint; + final int autoplay; + final int ugcPay; + final int isCooperation; + final int ugcPayPreview; + final int noBackground; + final int arcPay; + final int payFreeWatch; + + factory Rights.fromJson(Map json) => Rights( + bp: json["bp"], + elec: json["elec"], + download: json["download"], + movie: json["movie"], + pay: json["pay"], + hd5: json["hd5"], + noReprint: json["no_reprint"], + autoplay: json["autoplay"], + ugcPay: json["ugc_pay"], + isCooperation: json["is_cooperation"], + ugcPayPreview: json["ugc_pay_preview"], + noBackground: json["no_background"], + arcPay: json["arc_pay"], + payFreeWatch: json["pay_free_watch"], + ); +} + +class Stat { + Stat({ + required this.aid, + required this.view, + required this.danmaku, + required this.reply, + required this.favorite, + required this.coin, + required this.share, + required this.nowRank, + required this.hisRank, + required this.like, + required this.dislike, + required this.vt, + required this.vv, + }); + + final int aid; + final int view; + final int danmaku; + final int reply; + final int favorite; + final int coin; + final int share; + final int nowRank; + final int hisRank; + final int like; + final int dislike; + final int vt; + final int vv; + + factory Stat.fromJson(Map json) => Stat( + aid: json["aid"], + view: json["view"], + danmaku: json["danmaku"], + reply: json["reply"], + favorite: json["favorite"], + coin: json["coin"], + share: json["share"], + nowRank: json["now_rank"], + hisRank: json["his_rank"], + like: json["like"], + dislike: json["dislike"], + vt: json["vt"], + vv: json["vv"], + ); +} diff --git a/lib/pages/member/controller.dart b/lib/pages/member/controller.dart index 0aa7166f..7db046d4 100644 --- a/lib/pages/member/controller.dart +++ b/lib/pages/member/controller.dart @@ -8,6 +8,7 @@ import 'package:pilipala/http/video.dart'; import 'package:pilipala/models/member/archive.dart'; import 'package:pilipala/models/member/coin.dart'; import 'package:pilipala/models/member/info.dart'; +import 'package:pilipala/models/member/like.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:share_plus/share_plus.dart'; @@ -25,6 +26,7 @@ class MemberController extends GetxController { RxInt attribute = (-1).obs; RxString attributeText = '关注'.obs; RxList recentCoinsList = [].obs; + RxList recentLikeList = [].obs; @override void onInit() { @@ -208,6 +210,14 @@ class MemberController extends GetxController { return res; } + // 请求点赞视频 + Future getRecentLikeVideo() async { + if (userInfo == null) return; + var res = await MemberHttp.getRecentLikeVideo(mid: mid); + recentLikeList.value = res['data']; + return res; + } + // 跳转查看动态 void pushDynamicsPage() => Get.toNamed('/memberDynamics?mid=$mid'); diff --git a/lib/pages/member/view.dart b/lib/pages/member/view.dart index bb0d92be..9c0da652 100644 --- a/lib/pages/member/view.dart +++ b/lib/pages/member/view.dart @@ -9,6 +9,7 @@ import 'package:pilipala/pages/member/index.dart'; import 'package:pilipala/utils/utils.dart'; import 'widgets/conis.dart'; +import 'widgets/like.dart'; import 'widgets/profile.dart'; import 'widgets/seasons.dart'; @@ -26,6 +27,7 @@ class _MemberPageState extends State late Future _futureBuilderFuture; late Future _memberSeasonsFuture; late Future _memberCoinsFuture; + late Future _memberLikeFuture; final ScrollController _extendNestCtr = ScrollController(); final StreamController appbarStream = StreamController(); late int mid; @@ -39,6 +41,7 @@ class _MemberPageState extends State _futureBuilderFuture = _memberController.getInfo(); _memberSeasonsFuture = _memberController.getMemberSeasons(); _memberCoinsFuture = _memberController.getRecentCoinVideo(); + _memberLikeFuture = _memberController.getRecentLikeVideo(); _extendNestCtr.addListener( () { final double offset = _extendNestCtr.position.pixels; @@ -162,6 +165,7 @@ class _MemberPageState extends State trailing: const Icon(Icons.arrow_forward_outlined, size: 19), ), + const Divider(height: 1, thickness: 0.1), /// 视频 ListTile( @@ -170,12 +174,10 @@ class _MemberPageState extends State trailing: const Icon(Icons.arrow_forward_outlined, size: 19), ), + const Divider(height: 1, thickness: 0.1), /// 专栏 - ListTile( - onTap: () {}, - title: const Text('Ta的专栏'), - ), + const ListTile(title: Text('Ta的专栏')), MediaQuery.removePadding( removeTop: true, removeBottom: true, @@ -218,12 +220,7 @@ class _MemberPageState extends State /// 最近投币 Obx( () => _memberController.recentCoinsList.isNotEmpty - ? ListTile( - onTap: () {}, - title: const Text('最近投币的视频'), - // trailing: const Icon(Icons.arrow_forward_outlined, - // size: 19), - ) + ? const ListTile(title: Text('最近投币的视频')) : const SizedBox(), ), MediaQuery.removePadding( @@ -257,13 +254,44 @@ class _MemberPageState extends State ), ), ), - // 最近点赞 - // ListTile( - // onTap: () {}, - // title: const Text('最近点赞的视频'), - // trailing: - // const Icon(Icons.arrow_forward_outlined, size: 19), - // ), + + /// 最近点赞 + Obx( + () => _memberController.recentLikeList.isNotEmpty + ? const ListTile(title: Text('最近点赞的视频')) + : const SizedBox(), + ), + MediaQuery.removePadding( + removeTop: true, + removeBottom: true, + context: context, + child: Padding( + padding: const EdgeInsets.only( + left: StyleString.safeSpace, + right: StyleString.safeSpace, + ), + child: FutureBuilder( + future: _memberLikeFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.done) { + if (snapshot.data == null) { + return const SizedBox(); + } + if (snapshot.data['status']) { + Map data = snapshot.data as Map; + return MemberLikePanel(data: data['data']); + } else { + // 请求错误 + return const SizedBox(); + } + } else { + return const SizedBox(); + } + }, + ), + ), + ), ], ), ), diff --git a/lib/pages/member/widgets/conis.dart b/lib/pages/member/widgets/conis.dart index 57a8a583..bdff2120 100644 --- a/lib/pages/member/widgets/conis.dart +++ b/lib/pages/member/widgets/conis.dart @@ -4,8 +4,8 @@ import 'package:pilipala/models/member/coin.dart'; import 'package:pilipala/pages/member_coin/widgets/item.dart'; class MemberCoinsPanel extends StatelessWidget { - final List? data; - const MemberCoinsPanel({super.key, this.data}); + final List data; + const MemberCoinsPanel({super.key, required this.data}); @override Widget build(BuildContext context) { @@ -20,9 +20,9 @@ class MemberCoinsPanel extends StatelessWidget { ), physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, - itemCount: data!.length, + itemCount: data.length, itemBuilder: (context, i) { - return MemberCoinsItem(coinItem: data![i]); + return MemberCoinsItem(coinItem: data[i]); }, ); }, diff --git a/lib/pages/member/widgets/like.dart b/lib/pages/member/widgets/like.dart new file mode 100644 index 00000000..6342c274 --- /dev/null +++ b/lib/pages/member/widgets/like.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/models/member/like.dart'; +import 'package:pilipala/pages/member_like/widgets/item.dart'; + +class MemberLikePanel extends StatelessWidget { + final List data; + const MemberLikePanel({super.key, required this.data}); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, boxConstraints) { + return GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, // Use a fixed count for GridView + crossAxisSpacing: StyleString.safeSpace, + mainAxisSpacing: StyleString.safeSpace, + childAspectRatio: 0.94, + ), + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: data.length, + itemBuilder: (context, i) { + return MemberLikeItem(likeItem: data[i]); + }, + ); + }, + ); + } +} diff --git a/lib/pages/member_like/widgets/item.dart b/lib/pages/member_like/widgets/item.dart new file mode 100644 index 00000000..57798bb7 --- /dev/null +++ b/lib/pages/member_like/widgets/item.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/common/widgets/badge.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/common/widgets/stat/view.dart'; +import 'package:pilipala/http/search.dart'; +import 'package:pilipala/models/member/like.dart'; +import 'package:pilipala/utils/utils.dart'; + +class MemberLikeItem extends StatelessWidget { + final MemberLikeDataModel likeItem; + + const MemberLikeItem({ + Key? key, + required this.likeItem, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + String heroTag = Utils.makeHeroTag(likeItem.aid); + return Card( + elevation: 0, + clipBehavior: Clip.hardEdge, + margin: EdgeInsets.zero, + child: InkWell( + onTap: () async { + int cid = + await SearchHttp.ab2c(aid: likeItem.aid, bvid: likeItem.bvid); + Get.toNamed('/video?bvid=${likeItem.bvid}&cid=$cid', + arguments: {'videoItem': likeItem, 'heroTag': heroTag}); + }, + child: Column( + children: [ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder(builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + return Stack( + children: [ + NetworkImgLayer( + src: likeItem.pic, + width: maxWidth, + height: maxHeight, + ), + if (likeItem.duration != null) + PBadge( + bottom: 6, + right: 6, + type: 'gray', + text: Utils.timeFormat(likeItem.duration), + ) + ], + ); + }), + ), + Padding( + padding: const EdgeInsets.fromLTRB(5, 6, 0, 0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + likeItem.title!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Row( + children: [ + StatView( + view: likeItem.stat!.view, + theme: 'gray', + ), + const Spacer(), + Text( + Utils.CustomStamp_str( + timestamp: likeItem.pubdate, date: 'MM-DD'), + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.outline, + ), + ), + const SizedBox(width: 6) + ], + ), + ], + ), + ), + ], + ), + ), + ); + } +} From ea7ae15384660071505332282154a6d8f2037ed9 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sat, 29 Jun 2024 14:29:43 +0800 Subject: [PATCH 19/19] =?UTF-8?q?mod:=20=E5=90=88=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/member/controller.dart | 7 +++- lib/pages/member/view.dart | 52 +++++++++++++-------------- lib/pages/member/widgets/seasons.dart | 44 +++++++++++++---------- lib/pages/member_seasons/view.dart | 3 +- 4 files changed, 58 insertions(+), 48 deletions(-) diff --git a/lib/pages/member/controller.dart b/lib/pages/member/controller.dart index 7db046d4..cc928a8d 100644 --- a/lib/pages/member/controller.dart +++ b/lib/pages/member/controller.dart @@ -192,12 +192,17 @@ class MemberController extends GetxController { Share.share('${memberInfo.value.name} - https://space.bilibili.com/$mid'); } - // 请求专栏 + // 请求合集 Future getMemberSeasons() async { if (userInfo == null) return; var res = await MemberHttp.getMemberSeasons(mid, 1, 10); if (!res['status']) { SmartDialog.showToast("用户专栏请求异常:${res['msg']}"); + } else { + // 只取前四个专栏 + res['data'].seasonsList.map((e) { + e.archives = e.archives!.sublist(0, 4); + }).toList(); } return res; } diff --git a/lib/pages/member/view.dart b/lib/pages/member/view.dart index 9c0da652..f62ffacc 100644 --- a/lib/pages/member/view.dart +++ b/lib/pages/member/view.dart @@ -178,39 +178,37 @@ class _MemberPageState extends State /// 专栏 const ListTile(title: Text('Ta的专栏')), + const Divider(height: 1, thickness: 0.1), + + /// 合集 + const ListTile(title: Text('Ta的合集')), MediaQuery.removePadding( removeTop: true, removeBottom: true, context: context, - child: Padding( - padding: const EdgeInsets.only( - left: StyleString.safeSpace, - right: StyleString.safeSpace, - ), - child: FutureBuilder( - future: _memberSeasonsFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == - ConnectionState.done) { - if (snapshot.data == null) { - return const SizedBox(); - } - if (snapshot.data['status']) { - Map data = snapshot.data as Map; - if (data['data'].seasonsList.isEmpty) { - return commenWidget('用户没有设置专栏'); - } else { - return MemberSeasonsPanel(data: data['data']); - } - } else { - // 请求错误 - return const SizedBox(); - } - } else { + child: FutureBuilder( + future: _memberSeasonsFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.done) { + if (snapshot.data == null) { return const SizedBox(); } - }, - ), + if (snapshot.data['status']) { + Map data = snapshot.data as Map; + if (data['data'].seasonsList.isEmpty) { + return commenWidget('用户没有设置合集'); + } else { + return MemberSeasonsPanel(data: data['data']); + } + } else { + // 请求错误 + return const SizedBox(); + } + } else { + return const SizedBox(); + } + }, ), ), diff --git a/lib/pages/member/widgets/seasons.dart b/lib/pages/member/widgets/seasons.dart index 125c978f..1367d6bd 100644 --- a/lib/pages/member/widgets/seasons.dart +++ b/lib/pages/member/widgets/seasons.dart @@ -25,7 +25,7 @@ class MemberSeasonsPanel extends StatelessWidget { children: [ ListTile( onTap: () => Get.toNamed( - '/memberSeasons?mid=${item.meta!.mid}&seasonId=${item.meta!.seasonId}'), + '/memberSeasons?mid=${item.meta!.mid}&seasonId=${item.meta!.seasonId}&seasonName=${item.meta!.name}'), title: Text( item.meta!.name!, maxLines: 1, @@ -44,24 +44,30 @@ class MemberSeasonsPanel extends StatelessWidget { ), ), const SizedBox(height: 10), - LayoutBuilder( - builder: (context, boxConstraints) { - return GridView.builder( - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, // Use a fixed count for GridView - crossAxisSpacing: StyleString.safeSpace, - mainAxisSpacing: StyleString.safeSpace, - childAspectRatio: 0.94, - ), - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: item.archives!.length, - itemBuilder: (context, i) { - return MemberSeasonsItem(seasonItem: item.archives![i]); - }, - ); - }, + Padding( + padding: const EdgeInsets.only( + left: StyleString.safeSpace, + right: StyleString.safeSpace, + ), + child: LayoutBuilder( + builder: (context, boxConstraints) { + return GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, // Use a fixed count for GridView + crossAxisSpacing: StyleString.safeSpace, + mainAxisSpacing: StyleString.safeSpace, + childAspectRatio: 0.94, + ), + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: item.archives!.length, + itemBuilder: (context, i) { + return MemberSeasonsItem(seasonItem: item.archives![i]); + }, + ); + }, + ), ), ], ), diff --git a/lib/pages/member_seasons/view.dart b/lib/pages/member_seasons/view.dart index 06944f10..556e2ec5 100644 --- a/lib/pages/member_seasons/view.dart +++ b/lib/pages/member_seasons/view.dart @@ -43,7 +43,8 @@ class _MemberSeasonsPageState extends State { appBar: AppBar( titleSpacing: 0, centerTitle: false, - title: Text('他的专栏', style: Theme.of(context).textTheme.titleMedium), + title: Text(Get.parameters['seasonName']!, + style: Theme.of(context).textTheme.titleMedium), ), body: Padding( padding: const EdgeInsets.only(