From a6ab72cadd4a560aaba44ba75eec2386b231894f Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sun, 17 Dec 2023 14:55:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B6=88=E6=81=AF=E5=88=86=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/http/api.dart | 12 + lib/http/msg.dart | 13 +- lib/models/msg/session.dart | 2 +- lib/pages/whisper/controller.dart | 27 ++- lib/pages/whisper/view.dart | 228 +++++++++++------- lib/pages/whisperDetail/controller.dart | 8 +- lib/pages/whisperDetail/view.dart | 76 ++++-- lib/pages/whisperDetail/widget/chat_item.dart | 37 +-- 8 files changed, 259 insertions(+), 144 deletions(-) diff --git a/lib/http/api.dart b/lib/http/api.dart index 9584f108..3db3c617 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -359,6 +359,18 @@ class Api { static const String sessionMsg = 'https://api.vc.bilibili.com/svr_sync/v1/svr_sync/fetch_session_msgs'; + + /// 标记已读 POST + /// talker_id: + /// session_type: 1 + /// ack_seqno: 920224140918926 + /// build: 0 + /// mobi_app: web + /// csrf_token: + /// csrf: + static const String updateAck = + 'https://api.vc.bilibili.com/session_svr/v1/session_svr/update_ack'; + // 获取某个动态详情 // timezone_offset=-480 // id=849312409672744983 diff --git a/lib/http/msg.dart b/lib/http/msg.dart index 41cdefe8..3bf17aaf 100644 --- a/lib/http/msg.dart +++ b/lib/http/msg.dart @@ -6,16 +6,21 @@ import 'package:pilipala/utils/wbi_sign.dart'; class MsgHttp { // 会话列表 - static Future sessionList() async { - Map params = await WbiSign().makSign({ + static Future sessionList({int? endTs}) async { + Map params = { 'session_type': 1, 'group_fold': 1, 'unfollow_fold': 0, 'sort_rule': 2, 'build': 0, 'mobi_app': 'web', - }); - var res = await Request().get(Api.sessionList, data: params); + }; + if (endTs != null) { + params['end_ts'] = endTs; + } + + Map signParams = await WbiSign().makSign(params); + var res = await Request().get(Api.sessionList, data: signParams); if (res.data['code'] == 0) { return { 'status': true, diff --git a/lib/models/msg/session.dart b/lib/models/msg/session.dart index 683dd94a..706f0ae8 100644 --- a/lib/models/msg/session.dart +++ b/lib/models/msg/session.dart @@ -13,7 +13,7 @@ class SessionDataModel { SessionDataModel.fromJson(Map json) { sessionList = json['session_list'] - .map((e) => SessionList.fromJson(e)) + ?.map((e) => SessionList.fromJson(e)) .toList(); hasMore = json['has_more']; } diff --git a/lib/pages/whisper/controller.dart b/lib/pages/whisper/controller.dart index 3e0ae2bc..c82cab5b 100644 --- a/lib/pages/whisper/controller.dart +++ b/lib/pages/whisper/controller.dart @@ -6,10 +6,13 @@ import 'package:pilipala/models/msg/session.dart'; class WhisperController extends GetxController { RxList sessionList = [].obs; RxList accountList = [].obs; + bool isLoading = false; - Future querySessionList() async { - var res = await MsgHttp.sessionList(); - if (res['data'].sessionList.isNotEmpty) { + Future querySessionList(String? type) async { + if (isLoading) return; + var res = await MsgHttp.sessionList( + endTs: type == 'onLoad' ? sessionList.last.sessionTs : null); + if (res['data'].sessionList != null && res['data'].sessionList.isNotEmpty) { await queryAccountList(res['data'].sessionList); // 将 accountList 转换为 Map 结构 Map accountMap = {}; @@ -32,10 +35,14 @@ class WhisperController extends GetxController { } } } - if (res['status']) { - sessionList.value = res['data'].sessionList; + if (res['status'] && res['data'].sessionList != null) { + if (type == 'onLoad') { + sessionList.addAll(res['data'].sessionList); + } else { + sessionList.value = res['data'].sessionList; + } } - + isLoading = false; return res; } @@ -47,4 +54,12 @@ class WhisperController extends GetxController { } return res; } + + Future onLoad() async { + querySessionList('onLoad'); + } + + Future onRefresh() async { + querySessionList('onRefresh'); + } } diff --git a/lib/pages/whisper/view.dart b/lib/pages/whisper/view.dart index 7de3fa75..4de197ca 100644 --- a/lib/pages/whisper/view.dart +++ b/lib/pages/whisper/view.dart @@ -1,3 +1,4 @@ +import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; @@ -16,18 +17,31 @@ class _WhisperPageState extends State { late final WhisperController _whisperController = Get.put(WhisperController()); late Future _futureBuilderFuture; + final ScrollController _scrollController = ScrollController(); @override void initState() { super.initState(); - _futureBuilderFuture = _whisperController.querySessionList(); + _futureBuilderFuture = _whisperController.querySessionList('init'); + _scrollController.addListener(_scrollListener); + } + + Future _scrollListener() async { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 200) { + EasyThrottle.throttle('my-throttler', const Duration(milliseconds: 800), + () async { + await _whisperController.onLoad(); + _whisperController.isLoading = true; + }); + } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - scrolledUnderElevation: 0, + title: const Text('消息'), ), body: Column( children: [ @@ -82,95 +96,133 @@ class _WhisperPageState extends State { // }, // ), Expanded( - child: FutureBuilder( - future: _futureBuilderFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - Map data = snapshot.data as Map; - if (data['status']) { - List sessionList = _whisperController.sessionList; - return Obx( - () => sessionList.isEmpty - ? const SizedBox() - : ListView.builder( - itemCount: sessionList.length, - shrinkWrap: true, - itemBuilder: (_, int i) { - return ListTile( - onTap: () { - Get.toNamed( - '/whisperDetail?talkerId=${sessionList[i].talkerId}&name=${sessionList[i].accountInfo.name}'); - }, - leading: NetworkImgLayer( - width: 45, - height: 45, - type: 'avatar', - src: sessionList[i].accountInfo.face, - ), - title: Text( - sessionList[i].accountInfo.name, - style: - Theme.of(context).textTheme.titleSmall, - ), - subtitle: Text( - sessionList[i].lastMsg.content['text'] ?? - sessionList[i] - .lastMsg - .content['content'] ?? - sessionList[i] - .lastMsg - .content['title'], - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .labelMedium! - .copyWith( - color: Theme.of(context) - .colorScheme - .outline)), - trailing: Text( - Utils.dateFormat( - sessionList[i].lastMsg.timestamp), - style: Theme.of(context) - .textTheme - .labelSmall! - .copyWith( - color: Theme.of(context) - .colorScheme - .outline), - ), - ); - }, - ), - ); - } else { - // 请求错误 - return SizedBox(); - } - } else { - // 骨架屏 - return SizedBox(); - } + child: RefreshIndicator( + onRefresh: () async { + await _whisperController.onRefresh(); }, + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + children: [ + FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + List sessionList = _whisperController.sessionList; + return Obx( + () => sessionList.isEmpty + ? const SizedBox() + : ListView.separated( + itemCount: sessionList.length, + shrinkWrap: true, + physics: + const NeverScrollableScrollPhysics(), + itemBuilder: (_, int i) { + return ListTile( + onTap: () => Get.toNamed( + '/whisperDetail', + parameters: { + 'talkerId': sessionList[i] + .talkerId + .toString(), + 'name': sessionList[i] + .accountInfo + .name, + 'face': sessionList[i] + .accountInfo + .face, + 'mid': sessionList[i] + .accountInfo + .mid + .toString(), + }, + ), + leading: Badge( + isLabelVisible: false, + backgroundColor: Theme.of(context) + .colorScheme + .primary, + label: Text(sessionList[i] + .unreadCount + .toString()), + alignment: Alignment.bottomRight, + child: NetworkImgLayer( + width: 45, + height: 45, + type: 'avatar', + src: sessionList[i] + .accountInfo + .face, + ), + ), + title: Text( + sessionList[i].accountInfo.name), + subtitle: Text( + sessionList[i] + .lastMsg + .content['text'] ?? + sessionList[i] + .lastMsg + .content['content'] ?? + sessionList[i] + .lastMsg + .content['title'] ?? + sessionList[i] + .lastMsg + .content[ + 'reply_content'] ?? + '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .labelMedium! + .copyWith( + color: Theme.of(context) + .colorScheme + .outline)), + trailing: Text( + Utils.dateFormat(sessionList[i] + .lastMsg + .timestamp), + style: Theme.of(context) + .textTheme + .labelSmall! + .copyWith( + color: Theme.of(context) + .colorScheme + .outline), + ), + ); + }, + separatorBuilder: + (BuildContext context, int index) { + return Divider( + indent: 72, + endIndent: 20, + height: 6, + color: Colors.grey.withOpacity(0.1), + ); + }, + ), + ); + } else { + // 请求错误 + return SizedBox(); + } + } else { + // 骨架屏 + return SizedBox(); + } + }, + ) + ], + ), + ), ), ), - - // ListTile( - // onTap: () {}, - // leading: CircleAvatar(), - // title: Text('钱瑞昌'), - // subtitle: Text('没事', style: Theme.of(context).textTheme.bodySmall), - // trailing: Text('昨天'), - // ), - // ListTile( - // onTap: () {}, - // leading: CircleAvatar(), - // title: Text('李天'), - // subtitle: - // Text('明天有空吗', style: Theme.of(context).textTheme.bodySmall), - // trailing: Text('现在'), - // ) ], ), ); diff --git a/lib/pages/whisperDetail/controller.dart b/lib/pages/whisperDetail/controller.dart index ef25cfd4..52a70c49 100644 --- a/lib/pages/whisperDetail/controller.dart +++ b/lib/pages/whisperDetail/controller.dart @@ -4,14 +4,18 @@ import 'package:pilipala/models/msg/session.dart'; class WhisperDetailController extends GetxController { late int talkerId; - RxString name = ''.obs; + late String name; + late String face; + late String mid; RxList messageList = [].obs; @override void onInit() { super.onInit(); talkerId = int.parse(Get.parameters['talkerId']!); - name.value = Get.parameters['name']!; + name = Get.parameters['name']!; + face = Get.parameters['face']!; + mid = Get.parameters['mid']!; } Future querySessionMsg() async { diff --git a/lib/pages/whisperDetail/view.dart b/lib/pages/whisperDetail/view.dart index b2aba5da..0029eaff 100644 --- a/lib/pages/whisperDetail/view.dart +++ b/lib/pages/whisperDetail/view.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/pages/whisperDetail/controller.dart'; +import 'package:pilipala/utils/feed_back.dart'; import 'widget/chat_item.dart'; @@ -27,7 +29,6 @@ class _WhisperDetailPageState extends State { return Scaffold( appBar: AppBar( automaticallyImplyLeading: false, - scrolledUnderElevation: 0, title: SizedBox( width: double.infinity, height: 50, @@ -52,14 +53,35 @@ class _WhisperDetailPageState extends State { icon: Icon( Icons.arrow_back_outlined, size: 18, - color: Theme.of(context).colorScheme.primary, + color: Theme.of(context).colorScheme.onPrimaryContainer, ), ), ), - Obx( - () => Text( - _whisperDetailController.name.value, - style: Theme.of(context).textTheme.titleMedium, + GestureDetector( + onTap: () { + feedBack(); + Get.toNamed( + '/member?mid=${_whisperDetailController.mid}', + arguments: { + 'face': _whisperDetailController.face, + 'heroTag': null + }, + ); + }, + child: Row( + children: [ + NetworkImgLayer( + width: 34, + height: 34, + type: 'avatar', + src: _whisperDetailController.face, + ), + const SizedBox(width: 6), + Text( + _whisperDetailController.name, + style: Theme.of(context).textTheme.titleMedium, + ), + ], ), ), const SizedBox(width: 36, height: 36), @@ -105,12 +127,14 @@ class _WhisperDetailPageState extends State { } }, ), + // resizeToAvoidBottomInset: true, bottomNavigationBar: Container( width: double.infinity, height: MediaQuery.of(context).padding.bottom + 70, padding: EdgeInsets.only( left: 8, right: 12, + top: 12, bottom: MediaQuery.of(context).padding.bottom, ), decoration: BoxDecoration( @@ -124,6 +148,7 @@ class _WhisperDetailPageState extends State { ), child: Row( mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ // IconButton( // onPressed: () {}, @@ -139,25 +164,26 @@ class _WhisperDetailPageState extends State { color: Theme.of(context).colorScheme.outline, ), ), - // Expanded( - // child: Container( - // height: 42, - // decoration: BoxDecoration( - // color: Theme.of(context).colorScheme.primary.withOpacity(0.1), - // borderRadius: BorderRadius.circular(40.0), - // ), - // child: TextField( - // readOnly: true, - // style: Theme.of(context).textTheme.titleMedium, - // decoration: const InputDecoration( - // border: InputBorder.none, // 移除默认边框 - // hintText: '请输入内容', // 提示文本 - // contentPadding: EdgeInsets.symmetric( - // horizontal: 12.0, vertical: 12.0), // 内边距 - // ), - // ), - // ), - // ), + 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, + decoration: const InputDecoration( + border: InputBorder.none, // 移除默认边框 + hintText: '开发中 ...', // 提示文本 + contentPadding: EdgeInsets.symmetric( + horizontal: 16.0, vertical: 12.0), // 内边距 + ), + ), + ), + ), const SizedBox(width: 16), ], ), diff --git a/lib/pages/whisperDetail/widget/chat_item.dart b/lib/pages/whisperDetail/widget/chat_item.dart index 512300ce..7ac72aa5 100644 --- a/lib/pages/whisperDetail/widget/chat_item.dart +++ b/lib/pages/whisperDetail/widget/chat_item.dart @@ -17,6 +17,9 @@ class ChatItem extends StatelessWidget { bool isOwner = item.senderUid == 17340771; bool isPic = item.msgType == 2; bool isText = item.msgType == 1; + bool isAchive = item.msgType == 11; + bool isArticle = item.msgType == 12; + bool isSystem = item.msgType == 18 || item.msgType == 10 || item.msgType == 13; int msgType = item.msgType; @@ -115,7 +118,10 @@ class SystemNotice extends StatelessWidget { maxWidth: 300.0, // 设置最大宽度为200.0 ), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, + color: Theme.of(context) + .colorScheme + .secondaryContainer + .withOpacity(0.4), borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), @@ -128,25 +134,20 @@ class SystemNotice extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(content['title'], - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith(fontWeight: FontWeight.bold)), - Text( - Utils.dateFormat(item.timestamp), - style: Theme.of(context) - .textTheme - .labelSmall! - .copyWith(color: Theme.of(context).colorScheme.outline), - ) - ], + Text(content['title'], + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(fontWeight: FontWeight.bold)), + Text( + Utils.dateFormat(item.timestamp), + style: Theme.of(context) + .textTheme + .labelSmall! + .copyWith(color: Theme.of(context).colorScheme.outline), ), Divider( - color: Theme.of(context).colorScheme.primary.withOpacity(0.1), + color: Theme.of(context).colorScheme.primary.withOpacity(0.05), ), Text( content['text'],