Merge branch 'main' into design
This commit is contained in:
@ -326,6 +326,51 @@ class Api {
|
||||
// 获取指定分组下的up
|
||||
static const String followUpGroup = '/x/relation/tag';
|
||||
|
||||
/// 私聊
|
||||
/// 'https://api.vc.bilibili.com/session_svr/v1/session_svr/get_sessions?
|
||||
/// session_type=1&
|
||||
/// group_fold=1&
|
||||
/// unfollow_fold=0&
|
||||
/// sort_rule=2&
|
||||
/// build=0&
|
||||
/// mobi_app=web&
|
||||
/// w_rid=8641d157fb9a9255eb2159f316ee39e2&
|
||||
/// wts=1697305010
|
||||
|
||||
static const String sessionList =
|
||||
'https://api.vc.bilibili.com/session_svr/v1/session_svr/get_sessions';
|
||||
|
||||
/// 私聊用户信息
|
||||
/// uids
|
||||
/// build=0&mobi_app=web
|
||||
static const String sessionAccountList =
|
||||
'https://api.vc.bilibili.com/account/v1/user/cards';
|
||||
|
||||
/// https://api.vc.bilibili.com/svr_sync/v1/svr_sync/fetch_session_msgs?
|
||||
/// talker_id=400787461&
|
||||
/// session_type=1&
|
||||
/// size=20&
|
||||
/// sender_device_id=1&
|
||||
/// build=0&
|
||||
/// mobi_app=web&
|
||||
/// web_location=333.1296&
|
||||
/// w_rid=cfe3bf58c9fe181bbf4dd6c75175e6b0&
|
||||
/// wts=1697350697
|
||||
|
||||
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
|
||||
|
85
lib/http/msg.dart
Normal file
85
lib/http/msg.dart
Normal file
@ -0,0 +1,85 @@
|
||||
import 'package:pilipala/http/api.dart';
|
||||
import 'package:pilipala/http/init.dart';
|
||||
import 'package:pilipala/models/msg/account.dart';
|
||||
import 'package:pilipala/models/msg/session.dart';
|
||||
import 'package:pilipala/utils/wbi_sign.dart';
|
||||
|
||||
class MsgHttp {
|
||||
// 会话列表
|
||||
static Future sessionList({int? endTs}) async {
|
||||
Map<String, dynamic> params = {
|
||||
'session_type': 1,
|
||||
'group_fold': 1,
|
||||
'unfollow_fold': 0,
|
||||
'sort_rule': 2,
|
||||
'build': 0,
|
||||
'mobi_app': 'web',
|
||||
};
|
||||
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,
|
||||
'data': SessionDataModel.fromJson(res.data['data']),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': false,
|
||||
'date': [],
|
||||
'msg': res.data['message'],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static Future accountList(uids) async {
|
||||
var res = await Request().get(Api.sessionAccountList, data: {
|
||||
'uids': uids,
|
||||
'build': 0,
|
||||
'mobi_app': 'web',
|
||||
});
|
||||
if (res.data['code'] == 0) {
|
||||
return {
|
||||
'status': true,
|
||||
'data': res.data['data']
|
||||
.map<AccountListModel>((e) => AccountListModel.fromJson(e))
|
||||
.toList(),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': false,
|
||||
'date': [],
|
||||
'msg': res.data['message'],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static Future sessionMsg({
|
||||
int? talkerId,
|
||||
}) async {
|
||||
Map params = await WbiSign().makSign({
|
||||
'talker_id': talkerId,
|
||||
'session_type': 1,
|
||||
'size': 20,
|
||||
'sender_device_id': 1,
|
||||
'build': 0,
|
||||
'mobi_app': 'web',
|
||||
});
|
||||
var res = await Request().get(Api.sessionMsg, data: params);
|
||||
if (res.data['code'] == 0) {
|
||||
return {
|
||||
'status': true,
|
||||
'data': SessionMsgDataModel.fromJson(res.data['data']),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': false,
|
||||
'date': [],
|
||||
'msg': res.data['message'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
80
lib/models/msg/account.dart
Normal file
80
lib/models/msg/account.dart
Normal file
@ -0,0 +1,80 @@
|
||||
class AccountListModel {
|
||||
AccountListModel({
|
||||
this.mid,
|
||||
this.name,
|
||||
this.sex,
|
||||
this.face,
|
||||
this.sign,
|
||||
this.rank,
|
||||
this.level,
|
||||
this.silence,
|
||||
this.vip,
|
||||
this.pendant,
|
||||
this.nameplate,
|
||||
this.official,
|
||||
this.birthday,
|
||||
this.isFakeAccount,
|
||||
this.isDeleted,
|
||||
this.inRegAudit,
|
||||
this.faceNft,
|
||||
this.faceNftNew,
|
||||
this.isSeniorMember,
|
||||
this.digitalId,
|
||||
this.digitalType,
|
||||
this.attestation,
|
||||
this.expertInfo,
|
||||
this.honours,
|
||||
});
|
||||
|
||||
int? mid;
|
||||
String? name;
|
||||
String? sex;
|
||||
String? face;
|
||||
String? sign;
|
||||
int? rank;
|
||||
int? level;
|
||||
int? silence;
|
||||
Map? vip;
|
||||
Map? pendant;
|
||||
Map? nameplate;
|
||||
Map? official;
|
||||
int? birthday;
|
||||
int? isFakeAccount;
|
||||
int? isDeleted;
|
||||
int? inRegAudit;
|
||||
int? faceNft;
|
||||
int? faceNftNew;
|
||||
int? isSeniorMember;
|
||||
String? digitalId;
|
||||
int? digitalType;
|
||||
Map? attestation;
|
||||
Map? expertInfo;
|
||||
Map? honours;
|
||||
|
||||
AccountListModel.fromJson(Map<String, dynamic> json) {
|
||||
mid = json['mid'];
|
||||
name = json['name'] ?? '';
|
||||
sex = json['sex'];
|
||||
face = json['face'];
|
||||
sign = json['sign'];
|
||||
rank = json['rank'];
|
||||
level = json['level'];
|
||||
silence = json['silence'];
|
||||
vip = json['vip'];
|
||||
pendant = json['pendant'];
|
||||
nameplate = json['nameplate'];
|
||||
official = json['official'];
|
||||
birthday = json['birthday'];
|
||||
isFakeAccount = json['is_fake_account'];
|
||||
isDeleted = json['is_deleted'];
|
||||
inRegAudit = json['in_reg_audit'];
|
||||
faceNft = json['face_nft'];
|
||||
faceNftNew = json['face_nft_new'];
|
||||
isSeniorMember = json['is_senior_member'];
|
||||
digitalId = json['digital_id'];
|
||||
digitalType = json['digital_type'];
|
||||
attestation = json['attestation'];
|
||||
expertInfo = json['expert_info'];
|
||||
honours = json['honours'];
|
||||
}
|
||||
}
|
226
lib/models/msg/session.dart
Normal file
226
lib/models/msg/session.dart
Normal file
@ -0,0 +1,226 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:pilipala/models/msg/account.dart';
|
||||
|
||||
class SessionDataModel {
|
||||
SessionDataModel({
|
||||
this.sessionList,
|
||||
this.hasMore,
|
||||
});
|
||||
|
||||
List? sessionList;
|
||||
int? hasMore;
|
||||
|
||||
SessionDataModel.fromJson(Map<String, dynamic> json) {
|
||||
sessionList = json['session_list']
|
||||
?.map<SessionList>((e) => SessionList.fromJson(e))
|
||||
.toList();
|
||||
hasMore = json['has_more'];
|
||||
}
|
||||
}
|
||||
|
||||
class SessionList {
|
||||
SessionList({
|
||||
this.talkerId,
|
||||
this.sessionType,
|
||||
this.atSeqno,
|
||||
this.topTs,
|
||||
this.groupName,
|
||||
this.groupCover,
|
||||
this.isFollow,
|
||||
this.isDnd,
|
||||
this.ackSeqno,
|
||||
this.ackTs,
|
||||
this.sessionTs,
|
||||
this.unreadCount,
|
||||
this.lastMsg,
|
||||
this.groupType,
|
||||
this.canFold,
|
||||
this.status,
|
||||
this.maxSeqno,
|
||||
this.newPushMsg,
|
||||
this.setting,
|
||||
this.isGuardian,
|
||||
this.isIntercept,
|
||||
this.isTrust,
|
||||
this.systemMsgType,
|
||||
this.liveStatus,
|
||||
this.bizMsgUnreadCount,
|
||||
// this.userLabel,
|
||||
});
|
||||
|
||||
int? talkerId;
|
||||
int? sessionType;
|
||||
int? atSeqno;
|
||||
int? topTs;
|
||||
String? groupName;
|
||||
String? groupCover;
|
||||
int? isFollow;
|
||||
int? isDnd;
|
||||
int? ackSeqno;
|
||||
int? ackTs;
|
||||
int? sessionTs;
|
||||
int? unreadCount;
|
||||
LastMsg? lastMsg;
|
||||
int? groupType;
|
||||
int? canFold;
|
||||
int? status;
|
||||
int? maxSeqno;
|
||||
int? newPushMsg;
|
||||
int? setting;
|
||||
int? isGuardian;
|
||||
int? isIntercept;
|
||||
int? isTrust;
|
||||
int? systemMsgType;
|
||||
int? liveStatus;
|
||||
int? bizMsgUnreadCount;
|
||||
// int? userLabel;
|
||||
AccountListModel? accountInfo;
|
||||
|
||||
SessionList.fromJson(Map<String, dynamic> json) {
|
||||
talkerId = json["talker_id"];
|
||||
sessionType = json["session_type"];
|
||||
atSeqno = json["at_seqno"];
|
||||
topTs = json["top_ts"];
|
||||
groupName = json["group_name"];
|
||||
groupCover = json["group_cover"];
|
||||
isFollow = json["is_follow"];
|
||||
isDnd = json["is_dnd"];
|
||||
ackSeqno = json["ack_seqno"];
|
||||
ackTs = json["ack_ts"];
|
||||
sessionTs = json["session_ts"];
|
||||
unreadCount = json["unread_count"];
|
||||
lastMsg =
|
||||
json["last_msg"] != null ? LastMsg.fromJson(json["last_msg"]) : null;
|
||||
groupType = json["group_type"];
|
||||
canFold = json["can_fold"];
|
||||
status = json["status"];
|
||||
maxSeqno = json["max_seqno"];
|
||||
newPushMsg = json["new_push_msg"];
|
||||
setting = json["setting"];
|
||||
isGuardian = json["is_guardian"];
|
||||
isIntercept = json["is_intercept"];
|
||||
isTrust = json["is_trust"];
|
||||
systemMsgType = json["system_msg_type"];
|
||||
liveStatus = json["live_status"];
|
||||
bizMsgUnreadCount = json["biz_msg_unread_count"];
|
||||
// userLabel = json["user_label"];
|
||||
}
|
||||
}
|
||||
|
||||
class LastMsg {
|
||||
LastMsg({
|
||||
this.senderIid,
|
||||
this.receiverType,
|
||||
this.receiverId,
|
||||
this.msgType,
|
||||
this.content,
|
||||
this.msgSeqno,
|
||||
this.timestamp,
|
||||
this.atUids,
|
||||
this.msgKey,
|
||||
this.msgStatus,
|
||||
this.notifyCode,
|
||||
this.newFaceVersion,
|
||||
});
|
||||
|
||||
int? senderIid;
|
||||
int? receiverType;
|
||||
int? receiverId;
|
||||
int? msgType;
|
||||
Map? content;
|
||||
int? msgSeqno;
|
||||
int? timestamp;
|
||||
String? atUids;
|
||||
int? msgKey;
|
||||
int? msgStatus;
|
||||
String? notifyCode;
|
||||
int? newFaceVersion;
|
||||
|
||||
LastMsg.fromJson(Map<String, dynamic> json) {
|
||||
senderIid = json['sender_uid'];
|
||||
receiverType = json['receiver_type'];
|
||||
receiverId = json['receiver_id'];
|
||||
msgType = json['msg_type'];
|
||||
content = jsonDecode(json['content']);
|
||||
msgSeqno = json['msg_seqno'];
|
||||
timestamp = json['timestamp'];
|
||||
atUids = json['at_uids'];
|
||||
msgKey = json['msg_key'];
|
||||
msgStatus = json['msg_status'];
|
||||
notifyCode = json['notify_code'];
|
||||
newFaceVersion = json['new_face_version'];
|
||||
}
|
||||
}
|
||||
|
||||
class SessionMsgDataModel {
|
||||
SessionMsgDataModel({
|
||||
this.messages,
|
||||
this.hasMore,
|
||||
this.minSeqno,
|
||||
this.maxSeqno,
|
||||
this.eInfos,
|
||||
});
|
||||
|
||||
List<MessageItem>? messages;
|
||||
int? hasMore;
|
||||
int? minSeqno;
|
||||
int? maxSeqno;
|
||||
List? eInfos;
|
||||
|
||||
SessionMsgDataModel.fromJson(Map<String, dynamic> json) {
|
||||
messages = json['messages']
|
||||
.map<MessageItem>((e) => MessageItem.fromJson(e))
|
||||
.toList();
|
||||
hasMore = json['has_more'];
|
||||
minSeqno = json['min_seqno'];
|
||||
maxSeqno = json['max_seqno'];
|
||||
eInfos = json['e_infos'];
|
||||
}
|
||||
}
|
||||
|
||||
class MessageItem {
|
||||
MessageItem({
|
||||
this.senderUid,
|
||||
this.receiverType,
|
||||
this.receiverId,
|
||||
this.msgType,
|
||||
this.content,
|
||||
this.msgSeqno,
|
||||
this.timestamp,
|
||||
this.atUids,
|
||||
this.msgKey,
|
||||
this.msgStatus,
|
||||
this.notifyCode,
|
||||
this.newFaceVersion,
|
||||
});
|
||||
|
||||
int? senderUid;
|
||||
int? receiverType;
|
||||
int? receiverId;
|
||||
int? msgType;
|
||||
Map? content;
|
||||
int? msgSeqno;
|
||||
int? timestamp;
|
||||
List? atUids;
|
||||
int? msgKey;
|
||||
int? msgStatus;
|
||||
String? notifyCode;
|
||||
int? newFaceVersion;
|
||||
|
||||
MessageItem.fromJson(Map<String, dynamic> json) {
|
||||
senderUid = json['sender_uid'];
|
||||
receiverType = json['receiver_type'];
|
||||
receiverId = json['receiver_id'];
|
||||
// 1 文本 2 图片 18 系统提示 10 系统通知
|
||||
msgType = json['msg_type'];
|
||||
content = jsonDecode(json['content']);
|
||||
msgSeqno = json['msg_seqno'];
|
||||
timestamp = json['timestamp'];
|
||||
atUids = json['at_uids'];
|
||||
msgKey = json['msg_key'];
|
||||
msgStatus = json['msg_status'];
|
||||
notifyCode = json['notify_code'];
|
||||
newFaceVersion = json['new_face_version'];
|
||||
}
|
||||
}
|
@ -139,7 +139,13 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
const Expanded(child: SearchPage()),
|
||||
const SizedBox(width: 10),
|
||||
if (ctr!.userLogin.value) ...[
|
||||
const SizedBox(width: 6),
|
||||
IconButton(
|
||||
onPressed: () => Get.toNamed('/whisper'),
|
||||
icon: const Icon(Icons.notifications_none))
|
||||
],
|
||||
const SizedBox(width: 6),
|
||||
Obx(
|
||||
() => ctr!.userLogin.value
|
||||
? Stack(
|
||||
|
65
lib/pages/whisper/controller.dart
Normal file
65
lib/pages/whisper/controller.dart
Normal file
@ -0,0 +1,65 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/msg.dart';
|
||||
import 'package:pilipala/models/msg/account.dart';
|
||||
import 'package:pilipala/models/msg/session.dart';
|
||||
|
||||
class WhisperController extends GetxController {
|
||||
RxList<SessionList> sessionList = <SessionList>[].obs;
|
||||
RxList<AccountListModel> accountList = <AccountListModel>[].obs;
|
||||
bool isLoading = false;
|
||||
|
||||
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<int, dynamic> accountMap = {};
|
||||
for (var j in accountList) {
|
||||
accountMap[j.mid!] = j;
|
||||
}
|
||||
|
||||
// 遍历 sessionList,通过 mid 查找并赋值 accountInfo
|
||||
for (var i in res['data'].sessionList) {
|
||||
var accountInfo = accountMap[i.talkerId];
|
||||
if (accountInfo != null) {
|
||||
i.accountInfo = accountInfo;
|
||||
}
|
||||
if (i.talkerId == 844424930131966) {
|
||||
i.accountInfo = AccountListModel(
|
||||
name: 'UP主小助手',
|
||||
face:
|
||||
'https://message.biliimg.com/bfs/im/489a63efadfb202366c2f88853d2217b5ddc7a13.png',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
Future queryAccountList(sessionList) async {
|
||||
List midsList = sessionList.map((e) => e.talkerId!).toList();
|
||||
var res = await MsgHttp.accountList(midsList.join(','));
|
||||
if (res['status']) {
|
||||
accountList.value = res['data'];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
Future onLoad() async {
|
||||
querySessionList('onLoad');
|
||||
}
|
||||
|
||||
Future onRefresh() async {
|
||||
querySessionList('onRefresh');
|
||||
}
|
||||
}
|
4
lib/pages/whisper/index.dart
Normal file
4
lib/pages/whisper/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library whisper;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
230
lib/pages/whisper/view.dart
Normal file
230
lib/pages/whisper/view.dart
Normal file
@ -0,0 +1,230 @@
|
||||
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';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
|
||||
class WhisperPage extends StatefulWidget {
|
||||
const WhisperPage({super.key});
|
||||
|
||||
@override
|
||||
State<WhisperPage> createState() => _WhisperPageState();
|
||||
}
|
||||
|
||||
class _WhisperPageState extends State<WhisperPage> {
|
||||
late final WhisperController _whisperController =
|
||||
Get.put(WhisperController());
|
||||
late Future _futureBuilderFuture;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_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(
|
||||
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: 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();
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
28
lib/pages/whisperDetail/controller.dart
Normal file
28
lib/pages/whisperDetail/controller.dart
Normal file
@ -0,0 +1,28 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/msg.dart';
|
||||
import 'package:pilipala/models/msg/session.dart';
|
||||
|
||||
class WhisperDetailController extends GetxController {
|
||||
late int talkerId;
|
||||
late String name;
|
||||
late String face;
|
||||
late String mid;
|
||||
RxList<MessageItem> messageList = <MessageItem>[].obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
talkerId = int.parse(Get.parameters['talkerId']!);
|
||||
name = Get.parameters['name']!;
|
||||
face = Get.parameters['face']!;
|
||||
mid = Get.parameters['mid']!;
|
||||
}
|
||||
|
||||
Future querySessionMsg() async {
|
||||
var res = await MsgHttp.sessionMsg(talkerId: talkerId);
|
||||
if (res['status']) {
|
||||
messageList.value = res['data'].messages;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
4
lib/pages/whisperDetail/index.dart
Normal file
4
lib/pages/whisperDetail/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library whisper_detail;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
193
lib/pages/whisperDetail/view.dart
Normal file
193
lib/pages/whisperDetail/view.dart
Normal file
@ -0,0 +1,193 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/pages/whisperDetail/controller.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
|
||||
import 'widget/chat_item.dart';
|
||||
|
||||
class WhisperDetailPage extends StatefulWidget {
|
||||
const WhisperDetailPage({super.key});
|
||||
|
||||
@override
|
||||
State<WhisperDetailPage> createState() => _WhisperDetailPageState();
|
||||
}
|
||||
|
||||
class _WhisperDetailPageState extends State<WhisperDetailPage> {
|
||||
final WhisperDetailController _whisperDetailController =
|
||||
Get.put(WhisperDetailController());
|
||||
late Future _futureBuilderFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_futureBuilderFuture = _whisperDetailController.querySessionMsg();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
title: SizedBox(
|
||||
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((states) {
|
||||
return Theme.of(context)
|
||||
.colorScheme
|
||||
.primaryContainer
|
||||
.withOpacity(0.6);
|
||||
}),
|
||||
),
|
||||
onPressed: () => Get.back(),
|
||||
icon: Icon(
|
||||
Icons.arrow_back_outlined,
|
||||
size: 18,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
body: FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
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]),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return ChatItem(item: messageList[i]);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 请求错误
|
||||
return const SizedBox();
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
// 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(
|
||||
color: Colors.white,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
width: 4,
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// IconButton(
|
||||
// onPressed: () {},
|
||||
// icon: Icon(
|
||||
// Icons.add_circle_outline,
|
||||
// color: Theme.of(context).colorScheme.outline,
|
||||
// ),
|
||||
// ),
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: Icon(
|
||||
Icons.emoji_emotions_outlined,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
190
lib/pages/whisperDetail/widget/chat_item.dart
Normal file
190
lib/pages/whisperDetail/widget/chat_item.dart
Normal file
@ -0,0 +1,190 @@
|
||||
// ignore_for_file: must_be_immutable
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
class ChatItem extends StatelessWidget {
|
||||
dynamic item;
|
||||
|
||||
ChatItem({
|
||||
super.key,
|
||||
this.item,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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;
|
||||
Map content = item.content ?? '';
|
||||
return isSystem
|
||||
? (msgType == 10
|
||||
? SystemNotice(item: item)
|
||||
: msgType == 13
|
||||
? SystemNotice2(item: item)
|
||||
: 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),
|
||||
),
|
||||
),
|
||||
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: [
|
||||
isText
|
||||
? Text(
|
||||
content['content'],
|
||||
style: TextStyle(
|
||||
color: isOwner
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer),
|
||||
)
|
||||
: isPic
|
||||
? NetworkImgLayer(
|
||||
width: 220,
|
||||
height:
|
||||
220 * content['height'] / content['width'],
|
||||
src: content['url'],
|
||||
)
|
||||
: const SizedBox(),
|
||||
SizedBox(height: isPic ? 7 : 2),
|
||||
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)),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!isOwner) const Spacer(),
|
||||
if (isOwner) const SizedBox(width: 12),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SystemNotice extends StatelessWidget {
|
||||
dynamic item;
|
||||
SystemNotice({super.key, this.item});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Map content = item.content ?? '';
|
||||
return Row(
|
||||
children: [
|
||||
const SizedBox(width: 12),
|
||||
Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 300.0, // 设置最大宽度为200.0
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer
|
||||
.withOpacity(0.4),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(6),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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),
|
||||
),
|
||||
Divider(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.05),
|
||||
),
|
||||
Text(
|
||||
content['text'],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SystemNotice2 extends StatelessWidget {
|
||||
dynamic item;
|
||||
SystemNotice2({super.key, this.item});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Map content = item.content ?? '';
|
||||
return Row(
|
||||
children: [
|
||||
const SizedBox(width: 12),
|
||||
Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 300.0, // 设置最大宽度为200.0
|
||||
),
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: NetworkImgLayer(
|
||||
width: 320,
|
||||
height: 150,
|
||||
src: content['pic_url'],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -42,6 +42,8 @@ import 'package:pilipala/pages/video/detail/replyReply/index.dart';
|
||||
import 'package:pilipala/pages/webview/index.dart';
|
||||
import 'package:pilipala/pages/setting/index.dart';
|
||||
import 'package:pilipala/pages/media/index.dart';
|
||||
import 'package:pilipala/pages/whisper/index.dart';
|
||||
import 'package:pilipala/pages/whisperDetail/index.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
Box setting = GStrorage.setting;
|
||||
@ -127,6 +129,11 @@ class Routes {
|
||||
CustomGetPage(name: '/playSpeedSet', page: () => const PlaySpeedPage()),
|
||||
// 收藏搜索
|
||||
CustomGetPage(name: '/favSearch', page: () => const FavSearchPage()),
|
||||
// 消息页面
|
||||
CustomGetPage(name: '/whisper', page: () => const WhisperPage()),
|
||||
// 私信详情
|
||||
CustomGetPage(
|
||||
name: '/whisperDetail', page: () => const WhisperDetailPage()),
|
||||
// 登录页面
|
||||
CustomGetPage(name: '/loginPage', page: () => const LoginPage()),
|
||||
// 用户动态
|
||||
|
Reference in New Issue
Block a user