From 661e7bfa788d24c337a6f94870e8579a4d0c99e2 Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Tue, 6 May 2025 20:31:09 +0800 Subject: [PATCH] feat: live search Signed-off-by: bggRGjQaUbCoE --- lib/http/api.dart | 3 + lib/http/live.dart | 42 +++++ lib/models/common/live_search_type.dart | 1 + lib/models/live/live_search/data.dart | 39 +++++ lib/models/live/live_search/live_search.dart | 19 +++ lib/models/live/live_search/room.dart | 18 +++ lib/models/live/live_search/room_item.dart | 38 +++++ lib/models/live/live_search/user.dart | 17 ++ lib/models/live/live_search/user_item.dart | 33 ++++ lib/models/live/live_search/watched_show.dart | 26 +++ .../widgets/bangumi_card_v_member_home.dart | 36 ++--- .../widgets/bangumi_card_v_search.dart | 33 ++-- lib/pages/live/view.dart | 28 ++-- lib/pages/live_area/view.dart | 2 +- lib/pages/live_area_detail/view.dart | 3 +- lib/pages/live_search/child/controller.dart | 55 +++++++ lib/pages/live_search/child/view.dart | 151 ++++++++++++++++++ lib/pages/live_search/controller.dart | 63 ++++++++ lib/pages/live_search/view.dart | 103 ++++++++++++ .../live_search/widgets/live_search_room.dart | 114 +++++++++++++ .../live_search/widgets/live_search_user.dart | 66 ++++++++ lib/pages/member_article/view.dart | 4 +- lib/pages/member_search/controller.dart | 9 +- 23 files changed, 835 insertions(+), 68 deletions(-) create mode 100644 lib/models/common/live_search_type.dart create mode 100644 lib/models/live/live_search/data.dart create mode 100644 lib/models/live/live_search/live_search.dart create mode 100644 lib/models/live/live_search/room.dart create mode 100644 lib/models/live/live_search/room_item.dart create mode 100644 lib/models/live/live_search/user.dart create mode 100644 lib/models/live/live_search/user_item.dart create mode 100644 lib/models/live/live_search/watched_show.dart create mode 100644 lib/pages/live_search/child/controller.dart create mode 100644 lib/pages/live_search/child/view.dart create mode 100644 lib/pages/live_search/controller.dart create mode 100644 lib/pages/live_search/view.dart create mode 100644 lib/pages/live_search/widgets/live_search_room.dart create mode 100644 lib/pages/live_search/widgets/live_search_user.dart diff --git a/lib/http/api.dart b/lib/http/api.dart index d82f5fad..10821e57 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -819,4 +819,7 @@ class Api { static const String setLiveFavTag = '${HttpString.liveBaseUrl}/xlive/app-interface/v2/second/set_fav_tag'; + + static const String liveSearch = + '${HttpString.liveBaseUrl}/xlive/app-interface/v2/search_live'; } diff --git a/lib/http/live.dart b/lib/http/live.dart index 15222174..43a6bb21 100644 --- a/lib/http/live.dart +++ b/lib/http/live.dart @@ -2,6 +2,7 @@ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/http/api.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/common/live_search_type.dart'; import 'package:PiliPlus/models/live/live_area_list/area_item.dart'; import 'package:PiliPlus/models/live/live_area_list/area_list.dart'; import 'package:PiliPlus/models/live/live_emoticons/data.dart'; @@ -12,6 +13,7 @@ import 'package:PiliPlus/models/live/live_room/danmu_info.dart'; import 'package:PiliPlus/models/live/live_room/item.dart'; import 'package:PiliPlus/models/live/live_room/room_info.dart'; import 'package:PiliPlus/models/live/live_room/room_info_h5.dart'; +import 'package:PiliPlus/models/live/live_search/data.dart'; import 'package:PiliPlus/models/live/live_second_list/data.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/utils.dart'; @@ -432,4 +434,44 @@ class LiveHttp { return LoadingState.error(res.data['message']); } } + + static Future> liveSearch({ + required bool isLogin, + required int page, + required String keyword, + required LiveSearchType type, + }) async { + final params = { + if (isLogin) 'access_key': Accounts.main.accessKey, + 'appkey': Constants.appKey, + 'actionKey': 'appkey', + 'build': '8350200', + 'c_locale': 'zh_CN', + 'device': 'pad', + 'page': page, + 'pagesize': 30, + 'keyword': keyword, + 'disable_rcmd': '0', + 'mobi_app': 'android_hd', + 'platform': 'android', + 's_locale': 'zh_CN', + 'statistics': Constants.statistics, + 'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000, + 'type': type.name, + }; + Utils.appSign( + params, + Constants.appKey, + Constants.appSec, + ); + var res = await Request().get( + Api.liveSearch, + queryParameters: params, + ); + if (res.data['code'] == 0) { + return LoadingState.success(LiveSearchData.fromJson(res.data['data'])); + } else { + return LoadingState.error(res.data['message']); + } + } } diff --git a/lib/models/common/live_search_type.dart b/lib/models/common/live_search_type.dart new file mode 100644 index 00000000..adcd8cd7 --- /dev/null +++ b/lib/models/common/live_search_type.dart @@ -0,0 +1 @@ +enum LiveSearchType { room, user } diff --git a/lib/models/live/live_search/data.dart b/lib/models/live/live_search/data.dart new file mode 100644 index 00000000..117a78fc --- /dev/null +++ b/lib/models/live/live_search/data.dart @@ -0,0 +1,39 @@ +import 'room.dart'; +import 'user.dart'; + +class LiveSearchData { + String? type; + int? page; + int? pagesize; + Room? room; + User? user; + String? trackId; + String? abtestId; + String? query; + + LiveSearchData({ + this.type, + this.page, + this.pagesize, + this.room, + this.user, + this.trackId, + this.abtestId, + this.query, + }); + + factory LiveSearchData.fromJson(Map json) => LiveSearchData( + type: json['type'] as String?, + page: json['page'] as int?, + pagesize: json['pagesize'] as int?, + room: json['room'] == null + ? null + : Room.fromJson(json['room'] as Map), + user: json['user'] == null + ? null + : User.fromJson(json['user'] as Map), + trackId: json['track_id'] as String?, + abtestId: json['abtest_id'] as String?, + query: json['query'] as String?, + ); +} diff --git a/lib/models/live/live_search/live_search.dart b/lib/models/live/live_search/live_search.dart new file mode 100644 index 00000000..748d258b --- /dev/null +++ b/lib/models/live/live_search/live_search.dart @@ -0,0 +1,19 @@ +import 'data.dart'; + +class LiveSearch { + int? code; + String? message; + int? ttl; + LiveSearchData? data; + + LiveSearch({this.code, this.message, this.ttl, this.data}); + + factory LiveSearch.fromJson(Map json) => LiveSearch( + code: json['code'] as int?, + message: json['message'] as String?, + ttl: json['ttl'] as int?, + data: json['data'] == null + ? null + : LiveSearchData.fromJson(json['data'] as Map), + ); +} diff --git a/lib/models/live/live_search/room.dart b/lib/models/live/live_search/room.dart new file mode 100644 index 00000000..e52a0984 --- /dev/null +++ b/lib/models/live/live_search/room.dart @@ -0,0 +1,18 @@ +import 'room_item.dart'; + +class Room { + List? list; + int? totalRoom; + int? totalPage; + + Room({this.list, this.totalRoom, this.totalPage}); + + factory Room.fromJson(Map json) => Room( + list: (json['list'] as List?) + ?.map((e) => + LiveSearchRoomItemModel.fromJson(e as Map)) + .toList(), + totalRoom: json['total_room'] as int?, + totalPage: json['total_page'] as int?, + ); +} diff --git a/lib/models/live/live_search/room_item.dart b/lib/models/live/live_search/room_item.dart new file mode 100644 index 00000000..0db21603 --- /dev/null +++ b/lib/models/live/live_search/room_item.dart @@ -0,0 +1,38 @@ +import 'watched_show.dart'; + +class LiveSearchRoomItemModel { + int? roomid; + String? cover; + String? title; + String? name; + String? face; + int? online; + String? link; + WatchedShow? watchedShow; + + LiveSearchRoomItemModel({ + this.roomid, + this.cover, + this.title, + this.name, + this.face, + this.online, + this.link, + this.watchedShow, + }); + + factory LiveSearchRoomItemModel.fromJson(Map json) => + LiveSearchRoomItemModel( + roomid: json['roomid'] as int?, + cover: json['cover'] as String?, + title: json['title'] as String?, + name: json['name'] as String?, + face: json['face'] as String?, + online: json['online'] as int?, + link: json['link'] as String?, + watchedShow: json['watched_show'] == null + ? null + : WatchedShow.fromJson( + json['watched_show'] as Map), + ); +} diff --git a/lib/models/live/live_search/user.dart b/lib/models/live/live_search/user.dart new file mode 100644 index 00000000..9965040b --- /dev/null +++ b/lib/models/live/live_search/user.dart @@ -0,0 +1,17 @@ +import 'package:PiliPlus/models/live/live_search/user_item.dart'; + +class User { + List? list; + int? totalUser; + int? totalPage; + + User({this.list, this.totalUser, this.totalPage}); + + factory User.fromJson(Map json) => User( + list: (json['list'] as List?) + ?.map((e) => LiveSearchUserItemModel.fromJson(e)) + .toList(), + totalUser: json['total_user'] as int?, + totalPage: json['total_page'] as int?, + ); +} diff --git a/lib/models/live/live_search/user_item.dart b/lib/models/live/live_search/user_item.dart new file mode 100644 index 00000000..3a92df92 --- /dev/null +++ b/lib/models/live/live_search/user_item.dart @@ -0,0 +1,33 @@ +class LiveSearchUserItemModel { + String? face; + String? name; + int? liveStatus; + String? areaName; + int? fansNum; + List? roomTags; + int? roomid; + String? link; + + LiveSearchUserItemModel({ + this.face, + this.name, + this.liveStatus, + this.areaName, + this.fansNum, + this.roomTags, + this.roomid, + this.link, + }); + + factory LiveSearchUserItemModel.fromJson(Map json) => + LiveSearchUserItemModel( + face: json['face'] as String?, + name: json['name'] as String?, + liveStatus: json['live_status'] as int?, + areaName: json['areaName'] as String?, + fansNum: json['fansNum'] as int?, + roomTags: json['roomTags'], + roomid: json['roomid'] as int?, + link: json['link'] as String?, + ); +} diff --git a/lib/models/live/live_search/watched_show.dart b/lib/models/live/live_search/watched_show.dart new file mode 100644 index 00000000..f438c572 --- /dev/null +++ b/lib/models/live/live_search/watched_show.dart @@ -0,0 +1,26 @@ +class WatchedShow { + int? num; + String? textSmall; + String? textLarge; + String? icon; + int? iconLocation; + String? iconWeb; + + WatchedShow({ + this.num, + this.textSmall, + this.textLarge, + this.icon, + this.iconLocation, + this.iconWeb, + }); + + factory WatchedShow.fromJson(Map json) => WatchedShow( + num: json['num'] as int?, + textSmall: json['text_small'] as String?, + textLarge: json['text_large'] as String?, + icon: json['icon'] as String?, + iconLocation: json['icon_location'] as int?, + iconWeb: json['icon_web'] as String?, + ); +} diff --git a/lib/pages/bangumi/widgets/bangumi_card_v_member_home.dart b/lib/pages/bangumi/widgets/bangumi_card_v_member_home.dart index 8ae14d4d..997ee859 100644 --- a/lib/pages/bangumi/widgets/bangumi_card_v_member_home.dart +++ b/lib/pages/bangumi/widgets/bangumi_card_v_member_home.dart @@ -45,33 +45,21 @@ class BangumiCardVMemberHome extends StatelessWidget { ); }), ), - bangumiContent(bangumiItem) + Padding( + padding: const EdgeInsets.fromLTRB(4, 5, 0, 3), + child: Text( + bangumiItem.title, + textAlign: TextAlign.start, + style: const TextStyle( + letterSpacing: 0.3, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), ], ), ), ); } } - -Widget bangumiContent(SpaceArchiveItem bangumiItem) { - return Expanded( - child: Padding( - padding: const EdgeInsets.fromLTRB(4, 5, 0, 3), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - bangumiItem.title, - textAlign: TextAlign.start, - style: const TextStyle( - letterSpacing: 0.3, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 1), - ], - ), - ), - ); -} diff --git a/lib/pages/bangumi/widgets/bangumi_card_v_search.dart b/lib/pages/bangumi/widgets/bangumi_card_v_search.dart index 1e4dcb43..78fded7c 100644 --- a/lib/pages/bangumi/widgets/bangumi_card_v_search.dart +++ b/lib/pages/bangumi/widgets/bangumi_card_v_search.dart @@ -52,29 +52,18 @@ class BangumiCardVSearch extends StatelessWidget { ); }), ), - bagumiContent(context) - ], - ), - ), - ); - } - - Widget bagumiContent(context) { - return Expanded( - child: Padding( - padding: const EdgeInsets.fromLTRB(4, 5, 0, 3), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - item.title!.map((e) => e['text']).join(), - textAlign: TextAlign.start, - style: const TextStyle( - letterSpacing: 0.3, + Padding( + padding: const EdgeInsets.fromLTRB(4, 5, 0, 3), + child: Text( + item.title!.map((e) => e['text']).join(), + textAlign: TextAlign.start, + style: const TextStyle( + letterSpacing: 0.3, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), + ) ], ), ), diff --git a/lib/pages/live/view.dart b/lib/pages/live/view.dart index 09aaafa5..dd7ecfe8 100644 --- a/lib/pages/live/view.dart +++ b/lib/pages/live/view.dart @@ -37,6 +37,7 @@ class _LivePageState extends CommonPageState @override Widget build(BuildContext context) { super.build(context); + final ThemeData theme = Theme.of(context); return Container( clipBehavior: Clip.hardEdge, margin: const EdgeInsets.only( @@ -58,8 +59,8 @@ class _LivePageState extends CommonPageState bottom: MediaQuery.paddingOf(context).bottom + 80, ), sliver: SliverMainAxisGroup(slivers: [ - Obx(() => _buildTop(controller.topState.value)), - Obx(() => _buildBody(controller.loadingState.value)), + Obx(() => _buildTop(theme, controller.topState.value)), + Obx(() => _buildBody(theme, controller.loadingState.value)), ]), ), ], @@ -68,11 +69,11 @@ class _LivePageState extends CommonPageState ); } - Widget _buildTop(Pair data) { + Widget _buildTop(ThemeData theme, Pair data) { return SliverMainAxisGroup( slivers: [ if (data.first != null) - SliverToBoxAdapter(child: _buildFollowList(data.first!)), + SliverToBoxAdapter(child: _buildFollowList(theme, data.first!)), if (data.second?.cardData?.areaEntranceV3?.list?.isNotEmpty == true) SliverToBoxAdapter( child: Row( @@ -94,12 +95,10 @@ class _LivePageState extends CommonPageState ), text: index == 0 ? '推荐' : '${item.title}', bgColor: index == controller.areaIndex.value - ? Theme.of(context).colorScheme.secondaryContainer + ? theme.colorScheme.secondaryContainer : Colors.transparent, textColor: index == controller.areaIndex.value - ? Theme.of(context) - .colorScheme - .onSecondaryContainer + ? theme.colorScheme.onSecondaryContainer : null, onTap: (value) { controller.onSelectArea( @@ -133,7 +132,7 @@ class _LivePageState extends CommonPageState ); } - Widget _buildBody(LoadingState loadingState) { + Widget _buildBody(ThemeData theme, LoadingState loadingState) { return switch (loadingState) { Loading() => SliverGrid( gridDelegate: SliverGridDelegateWithExtentAndRatio( @@ -169,14 +168,10 @@ class _LivePageState extends CommonPageState ), text: '${item.name}', bgColor: index == controller.tagIndex.value - ? Theme.of(context) - .colorScheme - .secondaryContainer + ? theme.colorScheme.secondaryContainer : Colors.transparent, textColor: index == controller.tagIndex.value - ? Theme.of(context) - .colorScheme - .onSecondaryContainer + ? theme.colorScheme.onSecondaryContainer : null, onTap: (value) { controller.onSelectTag( @@ -224,8 +219,7 @@ class _LivePageState extends CommonPageState }; } - Widget _buildFollowList(LiveCardList item) { - final theme = Theme.of(context); + Widget _buildFollowList(ThemeData theme, LiveCardList item) { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/pages/live_area/view.dart b/lib/pages/live_area/view.dart index 49901437..48b6504e 100644 --- a/lib/pages/live_area/view.dart +++ b/lib/pages/live_area/view.dart @@ -316,8 +316,8 @@ class _LiveAreaPageState extends State { border: Border.all( color: theme.colorScheme.outline, ), - borderRadius: const BorderRadius.all(Radius.circular(4)), color: theme.colorScheme.surface, + borderRadius: const BorderRadius.all(Radius.circular(4)), ), child: SearchText( text: item.name!, diff --git a/lib/pages/live_area_detail/view.dart b/lib/pages/live_area_detail/view.dart index 6ea2ba9d..581b33f2 100644 --- a/lib/pages/live_area_detail/view.dart +++ b/lib/pages/live_area_detail/view.dart @@ -3,6 +3,7 @@ import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/live/live_area_list/area_item.dart'; import 'package:PiliPlus/pages/live_area_detail/child/view.dart'; import 'package:PiliPlus/pages/live_area_detail/controller.dart'; +import 'package:PiliPlus/pages/live_search/view.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -35,7 +36,7 @@ class _LiveAreaDetailPageState extends State { actions: [ IconButton( onPressed: () { - // TODO: search + Get.to(const LiveSearchPage()); }, icon: const Icon(Icons.search), ), diff --git a/lib/pages/live_search/child/controller.dart b/lib/pages/live_search/child/controller.dart new file mode 100644 index 00000000..ae5120a5 --- /dev/null +++ b/lib/pages/live_search/child/controller.dart @@ -0,0 +1,55 @@ +import 'package:PiliPlus/http/live.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/common/live_search_type.dart'; +import 'package:PiliPlus/models/live/live_search/data.dart'; +import 'package:PiliPlus/pages/common/common_list_controller.dart'; +import 'package:PiliPlus/pages/live_search/controller.dart'; +import 'package:PiliPlus/utils/storage.dart'; + +class LiveSearchChildController + extends CommonListController { + LiveSearchChildController(this.controller, this.searchType); + + final isLogin = Accounts.main.isLogin; + final LiveSearchController controller; + final LiveSearchType searchType; + + @override + void checkIsEnd(int length) { + switch (searchType) { + case LiveSearchType.room: + if (controller.counts.first != -1 && + length >= controller.counts.first) { + isEnd = true; + } + break; + case LiveSearchType.user: + if (controller.counts[1] != -1 && length >= controller.counts[1]) { + isEnd = true; + } + break; + } + } + + @override + List? getDataList(response) { + switch (searchType) { + case LiveSearchType.room: + controller.counts[searchType.index] = response.room?.totalRoom ?? 0; + return response.room?.list; + case LiveSearchType.user: + controller.counts[searchType.index] = response.user?.totalUser ?? 0; + return response.user?.list; + } + } + + @override + Future> customGetData() { + return LiveHttp.liveSearch( + isLogin: isLogin, + page: currentPage, + keyword: controller.editingController.text, + type: searchType, + ); + } +} diff --git a/lib/pages/live_search/child/view.dart b/lib/pages/live_search/child/view.dart new file mode 100644 index 00000000..c124b5df --- /dev/null +++ b/lib/pages/live_search/child/view.dart @@ -0,0 +1,151 @@ +import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/skeleton/msg_feed_top.dart'; +import 'package:PiliPlus/common/skeleton/video_card_v.dart'; +import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; +import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/common/live_search_type.dart'; +import 'package:PiliPlus/pages/live_search/child/controller.dart'; +import 'package:PiliPlus/pages/live_search/widgets/live_search_room.dart'; +import 'package:PiliPlus/pages/live_search/widgets/live_search_user.dart'; +import 'package:PiliPlus/utils/grid.dart'; +import 'package:PiliPlus/utils/storage.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class LiveSearchChildPage extends StatefulWidget { + const LiveSearchChildPage({ + super.key, + required this.controller, + required this.searchType, + }); + + final LiveSearchChildController controller; + final LiveSearchType searchType; + + @override + State createState() => _LiveSearchChildPageState(); +} + +class _LiveSearchChildPageState extends State + with AutomaticKeepAliveClientMixin { + late final bool dynamicsWaterfallFlow = GStorage.setting + .get(SettingBoxKey.dynamicsWaterfallFlow, defaultValue: true); + LiveSearchChildController get _controller => widget.controller; + + @override + Widget build(BuildContext context) { + super.build(context); + double padding = widget.searchType == LiveSearchType.room ? 12 : 0; + return refreshIndicator( + onRefresh: _controller.onRefresh, + child: CustomScrollView( + controller: _controller.scrollController, + slivers: [ + SliverPadding( + padding: EdgeInsets.only( + top: padding, + left: padding, + right: padding, + bottom: MediaQuery.paddingOf(context).bottom + 80, + ), + sliver: Obx(() => _buildBody(_controller.loadingState.value)), + ), + ], + ), + ); + } + + Widget get _buildLoading { + return switch (widget.searchType) { + LiveSearchType.room => SliverGrid( + gridDelegate: SliverGridDelegateWithExtentAndRatio( + mainAxisSpacing: StyleString.cardSpace, + crossAxisSpacing: StyleString.cardSpace, + maxCrossAxisExtent: Grid.smallCardWidth, + childAspectRatio: StyleString.aspectRatio, + mainAxisExtent: MediaQuery.textScalerOf(context).scale(90), + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + return const VideoCardVSkeleton(); + }, + childCount: 10, + ), + ), + LiveSearchType.user => SliverGrid( + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: Grid.smallCardWidth * 2, + mainAxisExtent: 60, + ), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + return const MsgFeedTopSkeleton(); + }, + childCount: 12, + ), + ), + }; + } + + Widget _buildBody(LoadingState loadingState) { + return switch (loadingState) { + Loading() => _buildLoading, + Success() => loadingState.response?.isNotEmpty == true + ? Builder( + builder: (context) { + return switch (widget.searchType) { + LiveSearchType.room => SliverGrid( + gridDelegate: SliverGridDelegateWithExtentAndRatio( + mainAxisSpacing: StyleString.cardSpace, + crossAxisSpacing: StyleString.cardSpace, + maxCrossAxisExtent: Grid.smallCardWidth, + childAspectRatio: StyleString.aspectRatio, + mainAxisExtent: + MediaQuery.textScalerOf(context).scale(60), + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == loadingState.response!.length - 1) { + _controller.onLoadMore(); + } + return LiveCardVSearch( + item: loadingState.response![index], + ); + }, + childCount: loadingState.response!.length, + ), + ), + LiveSearchType.user => SliverGrid( + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: Grid.smallCardWidth * 2, + mainAxisExtent: 60, + ), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + if (index == loadingState.response!.length - 1) { + _controller.onLoadMore(); + } + return LiveSearchUserItem( + item: loadingState.response![index], + ); + }, + childCount: loadingState.response!.length, + ), + ), + }; + }, + ) + : HttpError( + onReload: _controller.onReload, + ), + Error() => HttpError( + errMsg: loadingState.errMsg, + onReload: _controller.onReload, + ), + }; + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/pages/live_search/controller.dart b/lib/pages/live_search/controller.dart new file mode 100644 index 00000000..64bd2c8a --- /dev/null +++ b/lib/pages/live_search/controller.dart @@ -0,0 +1,63 @@ +import 'package:PiliPlus/models/common/live_search_type.dart'; +import 'package:PiliPlus/pages/live_search/child/controller.dart'; +import 'package:PiliPlus/utils/extension.dart'; +import 'package:PiliPlus/utils/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class LiveSearchController extends GetxController + with GetSingleTickerProviderStateMixin { + late final tabController = TabController(vsync: this, length: 2); + final editingController = TextEditingController(); + final focusNode = FocusNode(); + + final mid = Get.parameters['mid']; + final uname = Get.parameters['uname']; + + final RxBool hasData = false.obs; + final RxList counts = [-1, -1].obs; + + late final roomCtr = Get.put( + LiveSearchChildController(this, LiveSearchType.room), + tag: Utils.generateRandomString(8)); + late final userCtr = Get.put( + LiveSearchChildController(this, LiveSearchType.user), + tag: Utils.generateRandomString(8)); + + void onClear() { + if (editingController.value.text.isNotEmpty) { + editingController.clear(); + counts.value = [-1, -1]; + hasData.value = false; + focusNode.requestFocus(); + } else { + Get.back(); + } + } + + late final regex = RegExp(r'^\d+$'); + + void submit() { + if (editingController.text.isNotEmpty) { + if (regex.hasMatch(editingController.text)) { + Get.toNamed('/liveRoom?roomid=${editingController.text}'); + } else { + hasData.value = true; + roomCtr + ..scrollController.jumpToTop() + ..onReload(); + userCtr + ..scrollController.jumpToTop() + ..onReload(); + } + } + } + + @override + void onClose() { + editingController.dispose(); + focusNode.dispose(); + tabController.dispose(); + super.onClose(); + } +} diff --git a/lib/pages/live_search/view.dart b/lib/pages/live_search/view.dart new file mode 100644 index 00000000..95e251bc --- /dev/null +++ b/lib/pages/live_search/view.dart @@ -0,0 +1,103 @@ +import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/models/common/live_search_type.dart'; +import 'package:PiliPlus/pages/live_search/child/view.dart'; +import 'package:PiliPlus/pages/live_search/controller.dart'; +import 'package:PiliPlus/utils/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class LiveSearchPage extends StatefulWidget { + const LiveSearchPage({super.key}); + + @override + State createState() => _LiveSearchPageState(); +} + +class _LiveSearchPageState extends State { + final _controller = + Get.put(LiveSearchController(), tag: Utils.generateRandomString(8)); + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: false, + appBar: AppBar( + actions: [ + IconButton( + tooltip: '搜索', + onPressed: _controller.submit, + icon: const Icon(Icons.search, size: 22), + ), + const SizedBox(width: 10) + ], + title: TextField( + autofocus: true, + focusNode: _controller.focusNode, + controller: _controller.editingController, + textInputAction: TextInputAction.search, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + hintText: '搜索房间或主播', + border: InputBorder.none, + suffixIcon: IconButton( + tooltip: '清空', + icon: const Icon(Icons.clear, size: 22), + onPressed: _controller.onClear, + ), + ), + onSubmitted: (value) => _controller.submit(), + onChanged: (value) { + if (value.isEmpty) { + _controller.hasData.value = false; + } + }, + ), + ), + body: SafeArea( + top: false, + bottom: false, + child: Obx(() { + return Opacity( + opacity: _controller.hasData.value ? 1 : 0, + child: Column( + children: [ + TabBar( + controller: _controller.tabController, + tabs: [ + Obx( + () => Tab( + text: + '正在直播 ${_controller.counts[0] != -1 ? _controller.counts[0] : ''}', + ), + ), + Obx( + () => Tab( + text: + '主播 ${_controller.counts[1] != -1 ? _controller.counts[1] : ''}', + ), + ), + ], + ), + Expanded( + child: tabBarView( + controller: _controller.tabController, + children: [ + LiveSearchChildPage( + controller: _controller.roomCtr, + searchType: LiveSearchType.room, + ), + LiveSearchChildPage( + controller: _controller.userCtr, + searchType: LiveSearchType.user, + ), + ], + ), + ), + ], + ), + ); + }), + ), + ); + } +} diff --git a/lib/pages/live_search/widgets/live_search_room.dart b/lib/pages/live_search/widgets/live_search_room.dart new file mode 100644 index 00000000..6a9c0b8f --- /dev/null +++ b/lib/pages/live_search/widgets/live_search_room.dart @@ -0,0 +1,114 @@ +import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/image/image_save.dart'; +import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; +import 'package:PiliPlus/models/live/live_search/room_item.dart'; +import 'package:PiliPlus/utils/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +// 视频卡片 - 垂直布局 +class LiveCardVSearch extends StatelessWidget { + final LiveSearchRoomItemModel item; + + const LiveCardVSearch({ + super.key, + required this.item, + }); + + @override + Widget build(BuildContext context) { + String heroTag = Utils.makeHeroTag(item.roomid); + return Card( + clipBehavior: Clip.hardEdge, + margin: EdgeInsets.zero, + child: InkWell( + onTap: () { + Get.toNamed('/liveRoom?roomid=${item.roomid}'); + }, + onLongPress: () => imageSaveDialog( + title: item.title, + cover: item.cover, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder(builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + return Stack( + clipBehavior: Clip.none, + children: [ + Hero( + tag: heroTag, + child: NetworkImgLayer( + src: item.cover!, + width: maxWidth, + height: maxHeight, + ), + ), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: AnimatedOpacity( + opacity: 1, + duration: const Duration(milliseconds: 200), + child: videoStat(context), + ), + ), + ], + ); + }), + ), + Padding( + padding: const EdgeInsets.fromLTRB(5, 8, 5, 4), + child: Text( + '${item.title}', + textAlign: TextAlign.start, + style: const TextStyle( + letterSpacing: 0.3, + ), + maxLines: 2, + 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( + '${item.name}', + style: const TextStyle(fontSize: 11, color: Colors.white), + ), + if (item.watchedShow?.textSmall != null) + Text( + '${Utils.numFormat(item.watchedShow!.textSmall)}围观', + style: const TextStyle(fontSize: 11, color: Colors.white), + ), + ], + ), + ); + } +} diff --git a/lib/pages/live_search/widgets/live_search_user.dart b/lib/pages/live_search/widgets/live_search_user.dart new file mode 100644 index 00000000..cad9ae97 --- /dev/null +++ b/lib/pages/live_search/widgets/live_search_user.dart @@ -0,0 +1,66 @@ +import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; +import 'package:PiliPlus/models/live/live_search/user_item.dart'; +import 'package:PiliPlus/utils/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class LiveSearchUserItem extends StatelessWidget { + const LiveSearchUserItem({ + super.key, + required this.item, + }); + + final LiveSearchUserItemModel item; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final style = TextStyle( + fontSize: 13, + color: theme.colorScheme.outline, + ); + return InkWell( + onTap: () => Get.toNamed( + '/liveRoom?roomid=${item.roomid}', + ), + child: Row( + children: [ + const SizedBox(width: 15), + NetworkImgLayer( + src: item.face, + width: 42, + height: 42, + type: 'avatar', + ), + const SizedBox(width: 10), + Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + Text( + item.name!, + style: const TextStyle( + fontSize: 14, + ), + ), + if (item.liveStatus == 1) ...[ + const SizedBox(width: 10), + Image.asset(height: 14, 'assets/images/live/live.gif'), + ], + ], + ), + const SizedBox(height: 2), + Text( + '分区: ${item.areaName ?? ''} 关注数: ${Utils.numFormat(item.fansNum ?? 0)}', + style: style, + ), + ], + ) + ], + ), + ); + } +} diff --git a/lib/pages/member_article/view.dart b/lib/pages/member_article/view.dart index ce5f89f5..ff1ad9cd 100644 --- a/lib/pages/member_article/view.dart +++ b/lib/pages/member_article/view.dart @@ -50,7 +50,9 @@ class _MemberArticleState extends State slivers: [ SliverPadding( padding: EdgeInsets.only( - bottom: MediaQuery.paddingOf(context).bottom + 80), + top: 7, + bottom: MediaQuery.paddingOf(context).bottom + 80, + ), sliver: SliverGrid( gridDelegate: Grid.videoCardHDelegate(context), delegate: SliverChildBuilderDelegate( diff --git a/lib/pages/member_search/controller.dart b/lib/pages/member_search/controller.dart index 6982bfad..dd7b37dc 100644 --- a/lib/pages/member_search/controller.dart +++ b/lib/pages/member_search/controller.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/models/common/member/search_type.dart'; import 'package:PiliPlus/pages/member_search/child/controller.dart'; +import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -37,8 +38,12 @@ class MemberSearchController extends GetxController void submit() { if (editingController.text.isNotEmpty) { hasData.value = true; - arcCtr.onReload(); - dynCtr.onReload(); + arcCtr + ..scrollController.jumpToTop() + ..onReload(); + dynCtr + ..scrollController.jumpToTop() + ..onReload(); } }