diff --git a/lib/http/api.dart b/lib/http/api.dart index b0ddb682..9fa9306c 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -702,4 +702,8 @@ class Api { /// 稍后再看&收藏夹视频列表 static const String mediaList = '/x/v2/medialist/resource/list'; + + /// 我的关注 - 正在直播 + static const String getFollowingLive = + '${HttpString.liveBaseUrl}/xlive/web-ucenter/user/following'; } diff --git a/lib/http/live.dart b/lib/http/live.dart index 315fe130..9ee8aa41 100644 --- a/lib/http/live.dart +++ b/lib/http/live.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/live/danmu_info.dart'; +import 'package:PiliPlus/models/live/follow.dart'; import 'package:dio/dio.dart'; import '../models/live/item.dart'; import '../models/live/room_info.dart'; @@ -136,4 +137,29 @@ class LiveHttp { }; } } + + // 我的关注 正在直播 + static Future liveFollowing({required int pn, required int ps}) async { + var res = await Request().get( + Api.getFollowingLive, + queryParameters: { + 'page': pn, + 'page_size': ps, + 'platform': 'web', + 'ignoreRecord': 1, + 'hit_ab': true, + }, + ); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': LiveFollowingModel.fromJson(res.data['data']) + }; + } else { + return { + 'status': false, + 'msg': res.data['message'], + }; + } + } } diff --git a/lib/models/live/follow.dart b/lib/models/live/follow.dart new file mode 100644 index 00000000..ad466d77 --- /dev/null +++ b/lib/models/live/follow.dart @@ -0,0 +1,119 @@ +class LiveFollowingModel { + int? count; + List? list; + int? liveCount; + int? neverLivedCount; + List? neverLivedFaces; + int? pageSize; + String? title; + int? totalPage; + + LiveFollowingModel({ + this.count, + this.list, + this.liveCount, + this.neverLivedCount, + this.neverLivedFaces, + this.pageSize, + this.title, + this.totalPage, + }); + + LiveFollowingModel.fromJson(Map json) { + count = json['count']; + list = (json['list'] as List?) + ?.map((item) => LiveFollowingItemModel.fromJson(item)) + .toList() ?? + []; + liveCount = json['live_count']; + neverLivedCount = json['never_lived_count']; + neverLivedFaces = json['never_lived_faces']; + pageSize = json['pageSize']; + title = json['title']; + totalPage = json['totalPage']; + } +} + +class LiveFollowingItemModel { + int? roomId; + int? uid; + String? uname; + String? title; + String? face; + int? liveStatus; + int? recordNum; + String? recentRecordId; + int? isAttention; + int? clipNum; + int? fansNum; + String? areaName; + String? areaValue; + String? tags; + String? recentRecordIdV2; + int? recordNumV2; + int? recordLiveTime; + String? areaNameV2; + String? roomNews; + String? watchIcon; + String? textSmall; + String? roomCover; + String? pic; + int? parentAreaId; + int? areaId; + + LiveFollowingItemModel({ + this.roomId, + this.uid, + this.uname, + this.title, + this.face, + this.liveStatus, + this.recordNum, + this.recentRecordId, + this.isAttention, + this.clipNum, + this.fansNum, + this.areaName, + this.areaValue, + this.tags, + this.recentRecordIdV2, + this.recordNumV2, + this.recordLiveTime, + this.areaNameV2, + this.roomNews, + this.watchIcon, + this.textSmall, + this.roomCover, + this.pic, + this.parentAreaId, + this.areaId, + }); + + LiveFollowingItemModel.fromJson(Map json) { + roomId = json['roomid']; + uid = json['uid']; + uname = json['uname']; + title = json['title']; + face = json['face']; + liveStatus = json['live_status']; + recordNum = json['record_num']; + recentRecordId = json['recent_record_id']; + isAttention = json['is_attention']; + clipNum = json['clipnum']; + fansNum = json['fans_num']; + areaName = json['area_name']; + areaValue = json['area_value']; + tags = json['tags']; + recentRecordIdV2 = json['recent_record_id_v2']; + recordNumV2 = json['record_num_v2']; + recordLiveTime = json['record_live_time']; + areaNameV2 = json['area_name_v2']; + roomNews = json['room_news']; + watchIcon = json['watch_icon']; + textSmall = json['text_small']; + roomCover = json['room_cover']; + pic = json['room_cover']; + parentAreaId = json['parent_area_id']; + areaId = json['area_id']; + } +} diff --git a/lib/pages/live/controller.dart b/lib/pages/live/controller.dart index 3d217c38..af0be589 100644 --- a/lib/pages/live/controller.dart +++ b/lib/pages/live/controller.dart @@ -1,14 +1,62 @@ import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/live.dart'; +import 'package:PiliPlus/models/live/follow.dart'; import 'package:PiliPlus/pages/common/common_controller.dart'; +import 'package:PiliPlus/utils/extension.dart'; +import 'package:PiliPlus/utils/storage.dart'; +import 'package:get/get_rx/src/rx_types/rx_types.dart'; class LiveController extends CommonController { @override void onInit() { super.onInit(); queryData(); + if (isLogin.value) { + fetchLiveFollowing(); + } } @override Future customGetData() => LiveHttp.liveList(pn: currentPage); + + @override + Future onRefresh() { + fetchLiveFollowing(); + return super.onRefresh(); + } + + late RxBool isLogin = GStorage.isLogin.obs; + late Rx followListState = LoadingState.loading().obs; + late int followPage = 1; + late bool followEnd = false; + late RxInt liveCount = 0.obs; + + Future fetchLiveFollowing([bool isRefresh = true]) async { + if (isRefresh.not && followEnd) { + return; + } + if (isRefresh) { + followPage = 1; + followEnd = false; + } + dynamic res = await LiveHttp.liveFollowing(pn: followPage, ps: 20); + if (res['status']) { + followPage++; + liveCount.value = res['data'].liveCount; + List list = res['data'] + .list + .where((LiveFollowingItemModel item) => + item.liveStatus == 1 && item.recordLiveTime == 0) + .toList(); + if (isRefresh.not && followListState.value is Success) { + list.insertAll(0, (followListState.value as Success).response); + } + followEnd = list.length >= liveCount.value || + list.isEmpty || + res['data'].list.isNullOrEmpty; + followListState.value = LoadingState.success(list); + } else { + followListState.value = LoadingState.error(res['msg']); + } + } } diff --git a/lib/pages/live/view.dart b/lib/pages/live/view.dart index d5838fb3..a9ee4612 100644 --- a/lib/pages/live/view.dart +++ b/lib/pages/live/view.dart @@ -1,9 +1,14 @@ import 'dart:async'; +import 'package:PiliPlus/common/widgets/loading_widget.dart'; +import 'package:PiliPlus/common/widgets/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/self_sized_horizontal_list.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/pages/live/controller.dart'; import 'package:PiliPlus/pages/live/widgets/live_item.dart'; +import 'package:PiliPlus/pages/live/widgets/live_item_follow.dart'; +import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; @@ -75,10 +80,22 @@ class _LivePageState extends State controller: _controller.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ + Obx( + () => _controller.isLogin.value + ? SliverPadding( + padding: EdgeInsets.symmetric( + vertical: StyleString.cardSpace, + ), + sliver: SliverToBoxAdapter( + child: _buildFollowList(), + ), + ) + : const SliverToBoxAdapter(), + ), SliverPadding( padding: EdgeInsets.only( top: StyleString.cardSpace, - bottom: MediaQuery.paddingOf(context).bottom, + bottom: MediaQuery.paddingOf(context).bottom + 80, ), sliver: Obx( () => _controller.loadingState.value is Loading || @@ -126,4 +143,215 @@ class _LivePageState extends State ), ); } + + Widget _buildFollowList() { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Obx( + () => Text.rich( + TextSpan( + children: [ + TextSpan(text: '我的关注 '), + TextSpan( + text: '${_controller.liveCount.value}', + style: TextStyle( + fontSize: 13, + color: Theme.of(context).colorScheme.primary, + ), + ), + TextSpan( + text: '人正在直播', + style: TextStyle( + fontSize: 13, + color: Theme.of(context).colorScheme.outline, + ), + ), + ], + ), + ), + ), + const Spacer(), + GestureDetector( + onTap: () { + Get.to(_buildFollowListPage); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '查看更多', + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + ), + ), + Icon( + size: 20, + Icons.keyboard_arrow_right_outlined, + color: Theme.of(context).colorScheme.outline, + ), + ], + ), + ), + ], + ), + Obx(() => _buildFollowBody(_controller.followListState.value)), + ], + ); + } + + Widget _buildFollowBody(LoadingState loadingState) { + return switch (loadingState) { + Loading() => SizedBox( + height: 80, + child: loadingWidget, + ), + Success() => (loadingState.response as List?)?.isNotEmpty == true + ? SelfSizedHorizontalList( + gapSize: 5, + childBuilder: (index) { + if (index == loadingState.response.length - 1) { + _controller.fetchLiveFollowing(false); + } + return SizedBox( + width: 65, + child: GestureDetector( + onTap: () { + Get.toNamed( + '/liveRoom?roomid=${loadingState.response[index].roomId}', + arguments: { + 'liveItem': loadingState.response[index], + 'heroTag': + loadingState.response[index].roomId.toString() + }, + ); + }, + onLongPress: () { + Get.toNamed( + '/member?mid=${loadingState.response[index].uid}', + arguments: { + 'face': loadingState.response[index].face, + 'heroTag': Utils.makeHeroTag( + loadingState.response[index].uid) + }, + ); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 8), + Container( + margin: const EdgeInsets.all(2), + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + border: Border.all( + width: 1.5, + color: Theme.of(context).colorScheme.primary, + strokeAlign: BorderSide.strokeAlignOutside, + ), + shape: BoxShape.circle, + ), + child: NetworkImgLayer( + type: 'avatar', + width: 45, + height: 45, + src: loadingState.response[index].face, + ), + ), + const SizedBox(height: 2), + Text( + loadingState.response[index].uname, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 11), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + }, + itemCount: loadingState.response.length, + ) + : const SizedBox.shrink(), + Error() => GestureDetector( + onTap: () { + _controller.fetchLiveFollowing(); + }, + child: Container( + height: 80, + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + loadingState.errMsg, + textAlign: TextAlign.center, + ), + ), + ), + LoadingState() => throw UnimplementedError(), + }; + } + + Widget get _buildFollowListPage => Scaffold( + appBar: AppBar( + title: Obx( + () => Text('${_controller.liveCount.value}人正在直播'), + ), + ), + body: CustomScrollView( + slivers: [ + Obx( + () => _buildFollowListBody(_controller.followListState.value), + ), + ], + ), + ); + + Widget _buildFollowListBody(LoadingState loadingState) { + return switch (loadingState) { + Success() || Loading() => SliverPadding( + padding: EdgeInsets.only( + top: StyleString.cardSpace, + left: StyleString.cardSpace, + right: StyleString.cardSpace, + bottom: MediaQuery.paddingOf(context).bottom + 80), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithExtentAndRatio( + mainAxisSpacing: StyleString.cardSpace, + crossAxisSpacing: StyleString.cardSpace, + maxCrossAxisExtent: Grid.maxRowWidth, + childAspectRatio: StyleString.aspectRatio, + mainAxisExtent: MediaQuery.textScalerOf(context).scale(90), + ), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + if (loadingState is Success && + index == loadingState.response.length - 1) { + _controller.fetchLiveFollowing(false); + } + return loadingState is Success + ? LiveCardVFollow( + liveItem: loadingState.response[index], + ) + : const VideoCardVSkeleton(); + }, + childCount: + loadingState is Success ? loadingState.response.length : 10, + ), + ), + ), + Error() => HttpError( + errMsg: loadingState.errMsg, + callback: () { + _controller + ..followListState.value = LoadingState.loading() + ..fetchLiveFollowing(); + }, + ), + LoadingState() => throw UnimplementedError(), + }; + } } diff --git a/lib/pages/live/widgets/live_item.dart b/lib/pages/live/widgets/live_item.dart index 3ab3c65a..5e7d8416 100644 --- a/lib/pages/live/widgets/live_item.dart +++ b/lib/pages/live/widgets/live_item.dart @@ -22,7 +22,7 @@ class LiveCardV extends StatelessWidget { clipBehavior: Clip.hardEdge, margin: EdgeInsets.zero, child: InkWell( - onTap: () async { + onTap: () { Get.toNamed('/liveRoom?roomid=${liveItem.roomId}', arguments: {'liveItem': liveItem, 'heroTag': heroTag}); }, diff --git a/lib/pages/live/widgets/live_item_follow.dart b/lib/pages/live/widgets/live_item_follow.dart new file mode 100644 index 00000000..4b907535 --- /dev/null +++ b/lib/pages/live/widgets/live_item_follow.dart @@ -0,0 +1,159 @@ +import 'package:PiliPlus/common/widgets/image_save.dart'; +import 'package:PiliPlus/models/live/follow.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/utils/utils.dart'; +import 'package:PiliPlus/common/widgets/network_img_layer.dart'; + +// 视频卡片 - 垂直布局 +class LiveCardVFollow extends StatelessWidget { + final LiveFollowingItemModel liveItem; + + const LiveCardVFollow({ + super.key, + required this.liveItem, + }); + + @override + Widget build(BuildContext context) { + String heroTag = Utils.makeHeroTag(liveItem.roomId); + return Card( + clipBehavior: Clip.hardEdge, + margin: EdgeInsets.zero, + child: InkWell( + onTap: () { + Get.toNamed('/liveRoom?roomid=${liveItem.roomId}', + arguments: {'liveItem': liveItem, 'heroTag': heroTag}); + }, + onLongPress: () => imageSaveDialog( + context: context, + title: liveItem.title, + cover: liveItem.roomCover, + ), + child: Column( + children: [ + ClipRRect( + borderRadius: const BorderRadius.all(StyleString.imgRadius), + child: AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder(builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + return Stack( + children: [ + Hero( + tag: heroTag, + child: NetworkImgLayer( + src: liveItem.roomCover!, + width: maxWidth, + height: maxHeight, + ), + ), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: AnimatedOpacity( + opacity: 1, + duration: const Duration(milliseconds: 200), + child: videoStat(context), + ), + ), + ], + ); + }), + ), + ), + liveContent(context) + ], + ), + ), + ); + } + + Widget liveContent(context) { + return Expanded( + flex: 1, + child: Padding( + padding: const EdgeInsets.fromLTRB(5, 8, 5, 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${liveItem.title}', + textAlign: TextAlign.start, + style: const TextStyle( + letterSpacing: 0.3, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + Row( + children: [ + Expanded( + child: Text( + '${liveItem.uname}', + textAlign: TextAlign.start, + style: TextStyle( + fontSize: + Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget videoStat(context) { + return Container( + height: 50, + padding: const EdgeInsets.only(top: 26, left: 10, right: 10), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black54, + ], + tileMode: TileMode.mirror, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${liveItem.areaName}', + style: const TextStyle(fontSize: 11, color: Colors.white), + ), + Text( + liveItem.textSmall ?? '', + style: const TextStyle(fontSize: 11, color: Colors.white), + ), + ], + ), + + // child: RichText( + // maxLines: 1, + // textAlign: TextAlign.justify, + // softWrap: false, + // text: TextSpan( + // style: const TextStyle(fontSize: 11, color: Colors.white), + // children: [ + // TextSpan(text: liveItem!.areaName!), + // TextSpan(text: liveItem!.watchedShow!['text_small']), + // ], + // ), + // ), + ); + } +} diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index 9067b227..f98194ef 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -41,11 +41,12 @@ class LiveRoomController extends GetxController { if (Get.arguments != null) { liveItem = Get.arguments['liveItem']; heroTag = Get.arguments['heroTag'] ?? ''; - if (liveItem != null && liveItem.pic != null && liveItem.pic != '') { - cover = liveItem.pic; - } - if (liveItem != null && liveItem.cover != null && liveItem.cover != '') { - cover = liveItem.cover; + if (liveItem != null) { + cover = (liveItem.pic != null && liveItem.pic != '') + ? liveItem.pic + : (liveItem.cover != null && liveItem.cover != '') + ? liveItem.cover + : null; } } // CDN优化 diff --git a/lib/utils/login.dart b/lib/utils/login.dart index 3c912c6a..0215c844 100644 --- a/lib/utils/login.dart +++ b/lib/utils/login.dart @@ -6,6 +6,7 @@ import 'package:PiliPlus/models/common/dynamics_type.dart'; import 'package:PiliPlus/models/user/info.dart'; import 'package:PiliPlus/models/user/stat.dart'; import 'package:PiliPlus/pages/dynamics/tab/controller.dart'; +import 'package:PiliPlus/pages/live/controller.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:crypto/crypto.dart'; import 'package:get/get.dart'; @@ -45,6 +46,12 @@ class LoginUtils { ..loadingState.value = LoadingState.loading(); } catch (_) {} + try { + Get.find() + ..isLogin.value = false + ..loadingState.value = LoadingState.loading(); + } catch (_) {} + try { for (int i = 0; i < tabsConfig.length; i++) { Get.find(tag: tabsConfig[i]['tag']).onRefresh(); diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 3c86ba41..8be3b3c2 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -15,6 +15,7 @@ import 'package:PiliPlus/models/bangumi/info.dart'; import 'package:PiliPlus/models/common/search_type.dart'; import 'package:PiliPlus/pages/dynamics/controller.dart'; import 'package:PiliPlus/pages/home/controller.dart'; +import 'package:PiliPlus/pages/live/controller.dart'; import 'package:PiliPlus/pages/media/controller.dart'; import 'package:PiliPlus/pages/mine/controller.dart'; import 'package:PiliPlus/pages/video/detail/introduction/widgets/group_panel.dart'; @@ -148,6 +149,12 @@ class Utils { ..mid = result['data'].mid ..onRefresh(); } catch (_) {} + + try { + Get.find() + ..isLogin.value = true + ..fetchLiveFollowing(); + } catch (_) {} } else { // 获取用户信息失败 SmartDialog.showNotify(