flutter/lib/pages/my/vloger.dart
2025-09-22 14:41:47 +08:00

771 lines
30 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_typedefs/rx_typedefs.dart';
import 'package:loopin/IM/controller/chat_controller.dart';
import 'package:loopin/IM/im_service.dart';
import 'package:loopin/api/common_api.dart';
import 'package:loopin/api/video_api.dart';
import 'package:loopin/components/custom_sticky_header.dart';
import 'package:loopin/components/network_or_asset_image.dart';
import 'package:loopin/components/only_down_scroll_physics.dart';
import 'package:loopin/service/http.dart';
import 'package:loopin/styles/index.dart';
import 'package:loopin/utils/index.dart';
import 'package:nested_scroll_view_plus/nested_scroll_view_plus.dart';
import 'package:shirne_dialog/shirne_dialog.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_follow_info.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart';
class PageParams {
int page;
int pageSize;
bool isLoading;
bool hasMore;
PageParams({
this.page = 1,
this.pageSize = 10,
this.isLoading = false,
this.hasMore = true,
});
}
class Vloger extends StatefulWidget {
const Vloger({super.key});
@override
State<Vloger> createState() => MyPageState();
}
class MyPageState extends State<Vloger> with SingleTickerProviderStateMixin {
late dynamic args;
late RxInt currentTabIndex = 0.obs;
late RxList items = [].obs;
late RxList favoriteItems = [].obs;
late final Rx<V2TimUserFullInfo> userInfo = Rx(V2TimUserFullInfo(
userID: '',
nickName: '',
faceUrl: '',
selfSignature: '',
gender: 0,
customInfo: {
'coverBg': '',
},
role: 0,
));
late RxInt followed = 0.obs; // 是否关注
// followersCount粉丝多少人关注了我,mutualFollowersCount互关,followingCount我关注了多少人
late final Rx<V2TimFollowInfo> followInfo = Rx(V2TimFollowInfo(
followersCount: 0,
followingCount: 0,
));
RxBool get shouldFixHeader => (currentTabIndex.value == 0 && items.isEmpty) || (currentTabIndex.value == 1 && favoriteItems.isEmpty) ? true.obs : false.obs;
List tabList = [
{'name': "作品"},
];
late int vlogLikeCount = 0; // 点赞数量
late PageParams itemsParams;
late PageParams favoriteParams;
late TabController tabController;
late ScrollController scrollController;
late Callback tabListener;
late Callback scrollListener;
@override
void initState() {
super.initState();
args = Get.arguments ?? {};
print('argsssssssssssssssssssssss$args');
itemsParams = PageParams();
favoriteParams = PageParams();
selfInfo();
flowInfo();
checkFollowType();
initControllers();
scrollListener = () {
final pos = scrollController.position;
final isNearBottom = pos.pixels >= pos.maxScrollExtent - 100;
if (!isNearBottom) return;
if (currentTabIndex.value == 0 && !itemsParams.isLoading && itemsParams.hasMore) {
loadData(0);
} else if (currentTabIndex.value == 1 && !favoriteParams.isLoading && favoriteParams.hasMore) {
loadData(1);
}
};
scrollController.addListener(scrollListener);
tabListener = () {
currentTabIndex.value = tabController.index;
scrollController.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.easeIn);
loadData(0);
};
tabController.addListener(tabListener);
loadData(0);
getUserLikesCount();
}
@override
void dispose() {
tabController.removeListener(tabListener);
scrollController.removeListener(scrollListener);
tabController.dispose();
scrollController.dispose();
super.dispose();
}
// 获取用户的所有视频的点赞数量
void getUserLikesCount() async {
try {
final resData = await Http.get('${CommonApi.accountInfo}?memberId=${args['memberId']}');
print('aaaaaaaaaaaaaaaaaaa$resData');
if (resData != null && resData['code'] == 200) {
vlogLikeCount = resData['data']['vlogLikeCount'] ?? 0;
}
} catch (e) {}
}
void loadData([int? tabIndex]) async {
final index = tabIndex ?? currentTabIndex.value;
if (index == 0) {
if (itemsParams.isLoading || !itemsParams.hasMore) return;
itemsParams.isLoading = true;
final res = await Http.post(VideoApi.getVideoListByMemberId, data: {
"memberId": args['memberId'],
"current": itemsParams.page,
"size": itemsParams.pageSize,
});
final obj = res['data'];
final total = obj['total'];
final row = obj['records'] ?? [];
logger.i(res['data']);
// 判断是否还有更多数据
logger.e(items.length);
// 添加新数据,触发响应式更新
items.addAll(row);
logger.e(obj);
if (items.length >= total) {
itemsParams.hasMore = false;
}
// 页码加一
itemsParams.page++;
//
itemsParams.isLoading = false;
}
}
void initControllers() {
tabController = TabController(initialIndex: 0, length: tabList.length, vsync: this);
scrollController = ScrollController();
}
// 获取当前博主基本信息
void selfInfo() async {
final resIm = await ImService.instance.otherInfo(args['memberId']);
if (resIm.success && resIm.data != null) {
userInfo.value = resIm.data!;
logger.i(userInfo.value.toLogString());
} else {
logger.e(resIm.desc);
}
}
// 博主的关注与粉丝
void flowInfo() async {
logger.w(args.toString());
final res = await ImService.instance.getUserFollowInfo(userIDList: [args['memberId']]);
if (res.success && res.data?.first != null) {
//这里少个点赞,从服务端获取
// followersCount粉丝多少人关注了我,mutualFollowersCount互关,followingCount我关注了多少人
followInfo.value = res.data!.first;
logger.i(followInfo.value.toJson());
} else {
logger.e(res.desc);
}
}
// 检测当前用户是否关注博主
void checkFollowType() async {
/// 0不是好友也没有关注
/// 1你关注了对方单向
/// 2对方关注了你单向
/// 3互相关注双向好友
final res = await ImService.instance.checkFollowType(userIDList: [args['memberId']]);
if (res.success) {
final followType = res.data?.first.followType ?? 0;
logger.i(res.data?.first.toJson());
followed.value = followType;
logger.i(followed.value);
}
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: true,
onPopInvokedWithResult: (bool didPop, Object? result) {
if (didPop) {
print('User navigated back');
}
},
child: Scaffold(
backgroundColor: const Color(0xFFFAF6F9),
body: Obx(() {
return NestedScrollViewPlus(
controller: scrollController,
physics: shouldFixHeader.value ? const OnlyDownScrollPhysics(parent: AlwaysScrollableScrollPhysics()) : const AlwaysScrollableScrollPhysics(),
overscrollBehavior: OverscrollBehavior.outer,
headerSliverBuilder: (context, innerBoxIsScrolled) {
return [
SliverAppBar(
backgroundColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
expandedHeight: 180.0,
collapsedHeight: 120.0,
pinned: true,
stretch: true,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Get.back(result: {
'returnTo': '/',
'vlogerId': args['memberId'],
'followStatus': (followed.value == 1 || followed.value == 3) ? true : false,
});
},
),
onStretchTrigger: () async {
logger.i('触发 stretch 拉伸');
// 加载刷新逻辑
},
flexibleSpace: Obx(() {
userInfo.value;
return _buildFlexibleSpace();
}),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
children: [
Obx(() => _buildStatsCard()),
const SizedBox(height: 10.0),
Obx(() => _buildInfoDesc(context)),
const SizedBox(height: 10.0),
Obx(() => _buildFoucsButton(context)),
],
),
),
),
SliverPersistentHeader(
pinned: true,
delegate: CustomStickyHeader(
child: PreferredSize(
preferredSize: const Size.fromHeight(48.0),
child: Container(
color: Colors.white,
child: TabBar(
controller: tabController,
tabs: tabList.map((item) {
return Tab(
child: Badge.count(
backgroundColor: Colors.red,
count: item['badge'] ?? 0,
isLabelVisible: item['badge'] != null,
alignment: Alignment.topRight,
offset: const Offset(14, -6),
child: Text(item['name'], style: const TextStyle(fontWeight: FontWeight.bold)),
),
);
}).toList(),
isScrollable: true, //禁止左右滑动
tabAlignment: TabAlignment.start,
overlayColor: WidgetStateProperty.all(Colors.transparent),
unselectedLabelColor: Colors.black87,
labelColor: Colors.black,
indicator: const UnderlineTabIndicator(borderSide: BorderSide(color: Colors.transparent, width: 2.0)),
indicatorSize: TabBarIndicatorSize.label,
unselectedLabelStyle: const TextStyle(fontSize: 16.0, fontFamily: 'Microsoft YaHei'),
labelStyle: const TextStyle(fontSize: 18.0, fontFamily: 'Microsoft YaHei', fontWeight: FontWeight.bold),
dividerHeight: 0,
padding: const EdgeInsets.symmetric(horizontal: 10.0),
labelPadding: const EdgeInsets.symmetric(horizontal: 15.0),
),
),
),
),
),
];
},
body: TabBarView(
controller: tabController,
children: [
// Tab 1:
Obx(() => _buildGridTab(0)),
],
),
);
}),
),
);
}
// 空状态提示
Widget emptyTip(String text) {
return CustomScrollView(
physics: const OnlyDownScrollPhysics(),
slivers: [
SliverFillRemaining(
hasScrollBody: false,
child: Align(
alignment: Alignment.topCenter,
child: Padding(
padding: const EdgeInsets.only(top: 50.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset('assets/images/empty.png', width: 100.0),
const SizedBox(height: 8.0),
Text(
text,
style: const TextStyle(color: Colors.grey, fontSize: 13.0),
),
],
),
),
),
),
],
);
}
Widget _buildGridTab(int tabIndex) {
final listToShow = tabIndex == 0 ? items : favoriteItems;
final params = tabIndex == 0 ? itemsParams : favoriteParams;
if (listToShow.isEmpty) {
return emptyTip('暂无相关数据');
}
return CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsets.all(10.0),
sliver: SliverGrid(
delegate: SliverChildBuilderDelegate(
(context, index) {
final item = listToShow[index];
return GestureDetector(
onTap: () {
// 点击跳转到视频播放详情页面
Get.toNamed('/videoDetail', arguments: {'videoId': item['id']});
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10.0),
child: Stack(
fit: StackFit.expand,
children: [
// 视频封面图片
NetworkOrAssetImage(
imageUrl: item['firstFrameImg'] ?? item['cover'],
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
placeholderAsset: 'assets/images/bk.jpg',
),
// 半透明渐变底部背景
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
height: 40,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.6),
Colors.transparent,
],
),
),
),
),
// 右下角点赞数
Positioned(
right: 8,
bottom: 8,
child: Row(
children: [
const Icon(
Icons.favorite,
color: Colors.white,
size: 16,
),
const SizedBox(width: 4),
Text(
'${item['likeCounts'] ?? 0}',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
),
),
);
},
childCount: listToShow.length,
),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 10.0,
mainAxisSpacing: 10.0,
childAspectRatio: 0.7, // 调整为更适合视频封面的比例
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: Center(
child: params.hasMore ? const CircularProgressIndicator() : const Text('没有更多数据了'),
),
),
),
],
);
}
Widget _buildFlexibleSpace() {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final double maxHeight = 180.0;
final double minHeight = 100.0;
final double currentHeight = constraints.maxHeight;
double ratio = (currentHeight - minHeight) / (maxHeight - minHeight);
ratio = ratio.clamp(0.0, 1.0);
return Stack(
fit: StackFit.expand,
children: [
Positioned.fill(
child: Opacity(
opacity: 1.0,
child: NetworkOrAssetImage(
imageUrl: userInfo.value.customInfo?['coverBg'],
width: double.infinity,
placeholderAsset: 'assets/images/bk.jpg',
),
),
),
Positioned(
left: 15.0,
bottom: 0,
right: 15.0,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
ClipOval(
child: NetworkOrAssetImage(
imageUrl: userInfo.value.faceUrl,
width: 80,
height: 80,
),
),
const SizedBox(width: 15.0),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0),
decoration: BoxDecoration(color: Colors.black.withAlpha((0.3 * 255).round()), borderRadius: BorderRadius.circular(20.0)),
child: Text(
userInfo.value.nickName ?? '未知',
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
fontFamily: 'Arial',
color: Colors.white,
),
),
),
],
),
const SizedBox(height: 8.0),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0),
decoration: BoxDecoration(color: Colors.black.withAlpha((0.3 * 255).round()), borderRadius: BorderRadius.circular(20.0)),
child: InkWell(
onTap: () {
logger.i('点击个ID');
Clipboard.setData(ClipboardData(text: '${userInfo.value.userID}'));
MyDialog.toast('ID已复制', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.green.withAlpha(200)));
},
child: Text('ID:${userInfo.value.userID}', style: TextStyle(fontSize: 12.0, color: Colors.white)),
),
),
],
),
),
],
),
),
),
],
);
},
);
}
Widget _buildStatsCard() {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15.0),
boxShadow: [BoxShadow(color: Colors.black.withAlpha(10), offset: const Offset(0.0, 1.0), blurRadius: 2.0, spreadRadius: 0.0)],
),
clipBehavior: Clip.antiAlias,
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Column(children: [
Text(Utils.graceNumber(vlogLikeCount), style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)),
SizedBox(height: 3.0),
Text('获赞')
]),
Column(children: [
Text('${followInfo.value.followingCount}', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)),
SizedBox(height: 3.0),
Text('关注')
]),
Column(children: [
Text('${followInfo.value.followersCount}', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)),
SizedBox(height: 3.0),
Text('粉丝')
]),
],
),
),
);
}
Widget _buildInfoDesc(BuildContext context) {
final tx = userInfo.value.selfSignature;
if (tx == null || tx.isEmpty) {
return const SizedBox.shrink();
}
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15.0),
boxShadow: [BoxShadow(color: Colors.black.withAlpha(10), offset: const Offset(0.0, 1.0), blurRadius: 2.0, spreadRadius: 0.0)],
),
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(12),
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Text(
'${userInfo.value.selfSignature}',
style: const TextStyle(fontSize: 16),
),
),
],
));
}
/// 关注按钮
Widget _buildFoucsButton(BuildContext context) {
// final vlogerId = '1943510443312078850'; // 18832510385,后面改回博主的id
final vlogerId = args['memberId'];
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
final offsetAnimation = Tween<Offset>(
begin: Offset((followed.value == 0) ? -1 : 1, 0),
end: Offset.zero,
).animate(animation);
return SlideTransition(
position: offsetAnimation,
child: child,
);
},
child: [1, 2, 3].contains(followed.value)
? Row(
key: const ValueKey('followed'),
children: [
Expanded(
child: ElevatedButton(
onPressed: () async {
logger.w('点击已关注/已回关:${followed.value}');
final ctl = Get.find<ChatController>();
if (followed.value == 2) {
//回关,走关注逻辑
final res = await ImService.instance.followUser(userIDList: [vlogerId]);
if (res.success) {
followed.value = 3;
// 回关后如果会话存在将此会话移除noFriend会话组
final res = await ImService.instance.getConversation(conversationID: 'c2c_$vlogerId');
if (res.success) {
V2TimConversation conversation = res.data;
if (conversation.conversationGroupList?.isNotEmpty ?? false) {
//把会话数据从陌生人分组移除
await ImService.instance.deleteConversationsFromGroup(
groupName: conversation.conversationGroupList!.first!, // 不涉及到加入多分组的情况,直接取
conversationIDList: [conversation.conversationID],
);
ctl.updateNoFriendMenu();
}
}
}
} else {
final res = await ImService.instance.unfollowUser(userIDList: [vlogerId]);
if (res.success) {
// 如果为1那么状态置为0为3则置为2
followed.value = followed.value == 1 ? 0 : 2;
ctl.mergeNoFriend(conversationID: 'c2c_${userInfo.value.userID}');
}
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[300],
foregroundColor: Colors.black87,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(followed.value == 1
? '已关注'
: followed.value == 2
? '回关'
: followed.value == 3
? '已互关'
: '未知状态'),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () async {
logger.w('私信');
// 获取指定会话
final res = await ImService.instance.getConversation(conversationID: 'c2c_$vlogerId');
V2TimConversation conversation = res.data;
logger.i(conversation.toLogString());
if (res.success) {
// final isFriend = await ImService.instance.isMyFriend(vlogerId, FriendTypeEnum.V2TIM_FRIEND_TYPE_BOTH);
// 这里需要注意处理取关后重新关注逻辑
// 是否互相关注
if (followed.value == 3) {
Get.toNamed('/chat', arguments: conversation);
} else {
logger.i('对方没关注我');
logger.i(conversation.toLogString());
conversation.showName = conversation.showName ?? userInfo.value.nickName;
Get.toNamed('/chatNoFriend', arguments: conversation);
}
} else {
MyDialog.toast(res.desc, icon: const Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[300],
foregroundColor: Colors.black87,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text('私信'),
),
),
],
)
: SizedBox(
key: const ValueKey('not_followed'),
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () async {
// 0没关系2对方关注了我
logger.w('点击关注');
final res = await ImService.instance.followUser(userIDList: [vlogerId]);
if (res.success) {
followed.value = followed.value == 0 ? 1 : 3;
if (followed.value == 3) {
// 修改后若为3将此会话移除noFriend会话组
final res = await ImService.instance.getConversation(conversationID: 'c2c_$vlogerId');
if (res.success) {
V2TimConversation conversation = res.data;
if (conversation.conversationGroupList?.isNotEmpty == true) {
//移除陌生人会话
await ImService.instance.deleteConversationsFromGroup(
groupName: conversation.conversationGroupList!.first!,
conversationIDList: [conversation.conversationID],
);
}
}
}
}
},
icon: const Icon(Icons.add),
label: Text('关注'),
style: ElevatedButton.styleFrom(
backgroundColor: FStyle.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
);
}
}