diff --git a/lib/http/api.dart b/lib/http/api.dart index 1d5fff3ab..3b670e62b 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -259,6 +259,8 @@ class Api { // 分类搜索 static const String searchByType = '/x/web-interface/search/type'; + static const String searchAll = '/x/web-interface/search/all/v2'; + // 记录视频播放进度 // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/report.md static const String heartBeat = '/x/click-interface/web/heartbeat'; diff --git a/lib/http/search.dart b/lib/http/search.dart index d9467b08a..6f8012a96 100644 --- a/lib/http/search.dart +++ b/lib/http/search.dart @@ -43,7 +43,7 @@ class SearchHttp { } // 分类搜索 - static Future searchByType({ + static Future> searchByType({ required SearchType searchType, required String keyword, required page, @@ -56,13 +56,11 @@ class SearchHttp { int? pubBegin, int? pubEnd, }) async { - var reqData = { + var params = { 'search_type': searchType.name, 'keyword': keyword, - // 'order_sort': 0, - // 'user_type': 0, 'page': page, - if (order != null && order.isNotEmpty) 'order': order, + if (order?.isNotEmpty == true) 'order': order, if (duration != null) 'duration': duration, if (tids != null) 'tids': tids, if (orderSort != null) 'order_sort': orderSort, @@ -71,7 +69,10 @@ class SearchHttp { if (pubBegin != null) 'pubtime_begin_s': pubBegin, if (pubEnd != null) 'pubtime_end_s': pubEnd, }; - var res = await Request().get(Api.searchByType, queryParameters: reqData); + var res = await Request().get( + Api.searchByType, + queryParameters: params, + ); if (res.data is! Map) { return LoadingState.error('没有相关数据'); } @@ -101,6 +102,8 @@ class SearchHttp { case SearchType.article: data = SearchArticleModel.fromJson(res.data['data']); break; + // case SearchType.all: + // break; } return LoadingState.success(data); } catch (err) { @@ -108,10 +111,52 @@ class SearchHttp { return LoadingState.error(err.toString()); } } else { - return LoadingState.error( - res.data['data'] != null && res.data['data']['numPages'] == 0 - ? '没有相关数据' - : res.data['message']); + return LoadingState.error(res.data['message'] ?? '没有相关数据'); + } + } + + static Future searchAll({ + required String keyword, + required page, + String? order, + int? duration, + int? tids, + int? orderSort, + int? userType, + int? categoryId, + int? pubBegin, + int? pubEnd, + }) async { + var params = { + 'keyword': keyword, + 'page': page, + if (order?.isNotEmpty == true) 'order': order, + if (duration != null) 'duration': duration, + if (tids != null) 'tids': tids, + if (orderSort != null) 'order_sort': orderSort, + if (userType != null) 'user_type': userType, + if (categoryId != null) 'category_id': categoryId, + if (pubBegin != null) 'pubtime_begin_s': pubBegin, + if (pubEnd != null) 'pubtime_end_s': pubEnd, + }; + var res = await Request().get( + Api.searchByType, + queryParameters: params, + ); + if (res.data is! Map) { + return LoadingState.error('没有相关数据'); + } + if (res.data['code'] == 0) { + dynamic data; + try { + // TODO + return LoadingState.success(data); + } catch (err) { + debugPrint(err.toString()); + return LoadingState.error(err.toString()); + } + } else { + return LoadingState.error(res.data['message'] ?? '没有相关数据'); } } diff --git a/lib/models/common/search_type.dart b/lib/models/common/search_type.dart index b2d834415..13cfa21cc 100644 --- a/lib/models/common/search_type.dart +++ b/lib/models/common/search_type.dart @@ -1,5 +1,6 @@ // ignore_for_file: constant_identifier_names enum SearchType { + // all, // 视频:video video, // 番剧:media_bangumi, @@ -23,7 +24,15 @@ enum SearchType { } extension SearchTypeExtension on SearchType { - String get label => ['视频', '番剧', '影视', '直播间', '用户', '专栏'][index]; + String get label => [ + // '综合', + '视频', + '番剧', + '影视', + '直播间', + '用户', + '专栏', + ][index]; } // 搜索类型为视频、专栏及相簿时 diff --git a/lib/models/search/result.dart b/lib/models/search/result.dart index 64c48711c..01fb0976d 100644 --- a/lib/models/search/result.dart +++ b/lib/models/search/result.dart @@ -4,9 +4,21 @@ import 'package:PiliPlus/utils/utils.dart'; import '../model_owner.dart'; import '../model_video.dart'; -class SearchVideoModel { +abstract class SearchNumData { + SearchNumData({ + this.numResults, + this.list, + }); + int? numResults; - List? list; + List? list; +} + +class SearchVideoModel extends SearchNumData { + SearchVideoModel({ + super.numResults, + super.list, + }); SearchVideoModel.fromJson(Map json) { numResults = (json['numResults'] as num?)?.toInt(); @@ -88,15 +100,12 @@ class SearchOwner extends Owner { } } -class SearchUserModel { +class SearchUserModel extends SearchNumData { SearchUserModel({ - this.numResults, - this.list, + super.numResults, + super.list, }); - int? numResults; - List? list; - SearchUserModel.fromJson(Map json) { numResults = (json['numResults'] as num?)?.toInt(); list = (json['result'] as List?) @@ -165,15 +174,12 @@ class SearchUserItemModel { } } -class SearchLiveModel { +class SearchLiveModel extends SearchNumData { SearchLiveModel({ - this.numResults, - this.list, + super.numResults, + super.list, }); - int? numResults; - List? list; - SearchLiveModel.fromJson(Map json) { numResults = (json['numResults'] as num?)?.toInt(); list = json['result'] @@ -246,15 +252,12 @@ class SearchLiveItemModel { } } -class SearchMBangumiModel { +class SearchMBangumiModel extends SearchNumData { SearchMBangumiModel({ - this.numResults, - this.list, + super.numResults, + super.list, }); - int? numResults; - List? list; - SearchMBangumiModel.fromJson(Map json) { numResults = (json['numResults'] as num?)?.toInt(); list = (json['result'] as List?) @@ -348,15 +351,12 @@ class SearchMBangumiItemModel { } } -class SearchArticleModel { +class SearchArticleModel extends SearchNumData { SearchArticleModel({ - this.numResults, - this.list, + super.numResults, + super.list, }); - int? numResults; - List? list; - SearchArticleModel.fromJson(Map json) { numResults = (json['numResults'] as num?)?.toInt(); list = (json['result'] as List?) diff --git a/lib/pages/search_panel/article/controller.dart b/lib/pages/search_panel/article/controller.dart new file mode 100644 index 000000000..f07950683 --- /dev/null +++ b/lib/pages/search_panel/article/controller.dart @@ -0,0 +1,154 @@ +import 'dart:math'; + +import 'package:PiliPlus/models/search/result.dart'; +import 'package:PiliPlus/pages/search/widgets/search_text.dart'; +import 'package:PiliPlus/pages/search_panel/controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class SearchArticleController + extends SearchPanelController { + SearchArticleController({ + required super.keyword, + required super.searchType, + required super.tag, + }); + + @override + void onInit() { + super.onInit(); + jump2Article(); + } + + void jump2Article() { + String? cvid = RegExp(r'^cv(id)?(\d+)$', caseSensitive: false) + .firstMatch(keyword) + ?.group(2); + if (cvid != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Get.toNamed( + '/htmlRender', + parameters: { + 'url': 'https://www.bilibili.com/read/cv$cvid', + 'title': '', + 'id': 'cv$cvid', + 'dynamicType': 'read' + }, + ); + }); + } + } + + // sort + late final List orderFiltersList = [ + {'label': '综合排序', 'value': 0, 'order': 'totalrank'}, + {'label': '最新发布', 'value': 1, 'order': 'pubdate'}, + {'label': '最多点击', 'value': 2, 'order': 'click'}, + {'label': '最多喜欢', 'value': 3, 'order': 'attention'}, + {'label': '最多评论', 'value': 4, 'order': 'scores'}, + ]; + late final List zoneFiltersList = [ + {'label': '全部分区', 'value': 0, 'categoryId': 0}, + {'label': '动画', 'value': 1, 'categoryId': 2}, + {'label': '游戏', 'value': 2, 'categoryId': 1}, + {'label': '影视', 'value': 3, 'categoryId': 28}, + {'label': '生活', 'value': 4, 'categoryId': 3}, + {'label': '兴趣', 'value': 5, 'categoryId': 29}, + {'label': '轻小说', 'value': 6, 'categoryId': 16}, + {'label': '科技', 'value': 7, 'categoryId': 17}, + {'label': '笔记', 'value': 8, 'categoryId': 41}, + ]; + RxInt currentOrderFilterval = 0.obs; + RxInt currentZoneFilterval = 0.obs; + + onShowFilterDialog(BuildContext context) { + showModalBottomSheet( + context: context, + useSafeArea: true, + isScrollControlled: true, + clipBehavior: Clip.hardEdge, + backgroundColor: Theme.of(context).colorScheme.surface, + constraints: BoxConstraints( + maxWidth: min(640, min(Get.width, Get.height)), + ), + builder: (context) => SingleChildScrollView( + child: Container( + width: double.infinity, + padding: EdgeInsets.only( + top: 20, + left: 16, + right: 16, + bottom: 80 + MediaQuery.of(context).padding.bottom, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + const Text('排序', style: TextStyle(fontSize: 16)), + const SizedBox(height: 10), + Wrap( + spacing: 8, + runSpacing: 8, + children: orderFiltersList + .map( + (item) => SearchText( + text: item['label'], + onTap: (_) async { + Get.back(); + currentOrderFilterval.value = item['value']; + SmartDialog.dismiss(); + SmartDialog.showToast("「${item['label']}」的筛选结果"); + order.value = item['order']; + SmartDialog.showLoading(msg: 'loading'); + await onReload(); + SmartDialog.dismiss(); + }, + bgColor: item['value'] == currentOrderFilterval.value + ? Theme.of(context).colorScheme.secondaryContainer + : null, + textColor: item['value'] == currentOrderFilterval.value + ? Theme.of(context).colorScheme.onSecondaryContainer + : null, + ), + ) + .toList(), + ), + const SizedBox(height: 20), + const Text('分区', style: TextStyle(fontSize: 16)), + const SizedBox(height: 10), + Wrap( + spacing: 8, + runSpacing: 8, + children: zoneFiltersList + .map( + (item) => SearchText( + text: item['label'], + onTap: (_) async { + Get.back(); + currentZoneFilterval.value = item['value']; + SmartDialog.dismiss(); + SmartDialog.showToast("「${item['label']}」的筛选结果"); + categoryId = item['categoryId']; + SmartDialog.showLoading(msg: 'loading'); + await onReload(); + SmartDialog.dismiss(); + }, + bgColor: item['value'] == currentZoneFilterval.value + ? Theme.of(context).colorScheme.secondaryContainer + : null, + textColor: item['value'] == currentZoneFilterval.value + ? Theme.of(context).colorScheme.onSecondaryContainer + : null, + ), + ) + .toList(), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/search_panel/article/view.dart b/lib/pages/search_panel/article/view.dart new file mode 100644 index 000000000..796fc9adf --- /dev/null +++ b/lib/pages/search_panel/article/view.dart @@ -0,0 +1,225 @@ +import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; +import 'package:PiliPlus/common/widgets/image_save.dart'; +import 'package:PiliPlus/common/widgets/network_img_layer.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/search/result.dart'; +import 'package:PiliPlus/pages/search_panel/article/controller.dart'; +import 'package:PiliPlus/pages/search_panel/view.dart'; +import 'package:PiliPlus/utils/grid.dart'; +import 'package:PiliPlus/utils/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class SearchArticlePanel extends CommonSearchPanel { + const SearchArticlePanel({ + super.key, + required super.keyword, + required super.tag, + required super.searchType, + super.hasHeader = true, + }); + + @override + State createState() => _SearchArticlePanelState(); +} + +class _SearchArticlePanelState extends CommonSearchPanelState< + SearchArticlePanel, SearchArticleModel, SearchArticleItemModel> { + @override + late final SearchArticleController controller = Get.put( + SearchArticleController( + keyword: widget.keyword, + searchType: widget.searchType, + tag: widget.tag, + ), + tag: widget.searchType.name + widget.tag, + ); + + late TextStyle textStyle; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + textStyle = TextStyle( + fontSize: Theme.of(context).textTheme.labelSmall!.fontSize, + color: Theme.of(context).colorScheme.outline, + ); + } + + @override + Widget buildHeader(LoadingState?> loadingState) { + if (loadingState is Success) { + return SliverPersistentHeader( + pinned: false, + floating: true, + delegate: CustomSliverPersistentHeaderDelegate( + extent: 40, + bgColor: Theme.of(context).colorScheme.surface, + child: Container( + height: 40, + padding: const EdgeInsets.only(left: 25, right: 12), + child: Row( + children: [ + Obx( + () => Text( + '排序: ${controller.orderFiltersList[controller.currentOrderFilterval.value]['label']}', + maxLines: 1, + style: + TextStyle(color: Theme.of(context).colorScheme.outline), + ), + ), + const Spacer(), + Obx( + () => Text( + '分区: ${controller.zoneFiltersList[controller.currentZoneFilterval.value]['label']}', + maxLines: 1, + style: + TextStyle(color: Theme.of(context).colorScheme.outline), + ), + ), + const Spacer(), + SizedBox( + width: 32, + height: 32, + child: IconButton( + tooltip: '筛选', + style: ButtonStyle( + padding: WidgetStateProperty.all(EdgeInsets.zero), + ), + onPressed: () { + controller.onShowFilterDialog(context); + }, + icon: Icon( + Icons.filter_list_outlined, + size: 18, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ], + ), + ), + ), + ); + } + return const SliverToBoxAdapter(); + } + + late final TextStyle style = TextStyle(fontSize: 13); + + @override + Widget buildList(List list) { + return SliverPadding( + padding: const EdgeInsets.only(bottom: 80), + sliver: SliverGrid( + gridDelegate: Grid.videoCardHDelegate(context), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + if (index == list.length - 1) { + controller.onLoadMore(); + } + final item = list[index]; + return InkWell( + onTap: () { + Get.toNamed('/htmlRender', parameters: { + 'url': 'www.bilibili.com/read/cv${item.id}', + 'title': item.subTitle ?? '', + 'id': 'cv${item.id}', + 'dynamicType': 'read' + }); + }, + onLongPress: () => imageSaveDialog( + context: context, + title: item.title?.map((item) => item['text']).join() ?? '', + cover: item.imageUrls?.firstOrNull, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: StyleString.safeSpace, + vertical: 5, + ), + child: LayoutBuilder( + builder: (context, boxConstraints) { + final double width = (boxConstraints.maxWidth - + StyleString.cardSpace * + 6 / + MediaQuery.textScalerOf(context).scale(1.0)) / + 2; + return Container( + constraints: const BoxConstraints(minHeight: 88), + height: width / StyleString.aspectRatio, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (item.imageUrls?.isNotEmpty == true) + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder( + builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + return NetworkImgLayer( + width: maxWidth, + height: maxHeight, + src: item.imageUrls?.firstOrNull, + ); + }), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text.rich( + maxLines: 2, + TextSpan( + children: [ + for (var i in item.title!) ...[ + TextSpan( + text: i['text'], + style: TextStyle( + color: i['type'] == 'em' + ? Theme.of(context) + .colorScheme + .primary + : Theme.of(context) + .colorScheme + .onSurface, + ), + ), + ] + ], + ), + ), + const Spacer(), + Text( + Utils.dateFormat(item.pubTime, + formatType: 'detail'), + style: textStyle, + ), + Row( + children: [ + Text('${item.view}浏览', style: textStyle), + Text(' • ', style: textStyle), + Text('${item.reply}评论', style: textStyle), + ], + ), + ], + ), + ), + ], + ), + ); + }, + ), + ), + ); + }, + childCount: list.length, + ), + ), + ); + } +} diff --git a/lib/pages/search_panel/controller.dart b/lib/pages/search_panel/controller.dart index c0c52d78a..1f0842a03 100644 --- a/lib/pages/search_panel/controller.dart +++ b/lib/pages/search_panel/controller.dart @@ -1,32 +1,31 @@ import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/search/result.dart'; import 'package:PiliPlus/pages/common/common_list_controller.dart'; import 'package:PiliPlus/pages/search_result/controller.dart'; -import 'package:PiliPlus/utils/app_scheme.dart'; -import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:PiliPlus/http/search.dart'; import 'package:PiliPlus/models/common/search_type.dart'; -class SearchPanelController extends CommonListController { +class SearchPanelController, T> + extends CommonListController { SearchPanelController({ required this.keyword, required this.searchType, required this.tag, }); - String keyword; - SearchType searchType; - // 结果排序方式 搜索类型为视频、专栏及相簿时 - RxString order = ''.obs; - // 视频时长筛选 仅用于搜索视频 - RxInt duration = 0.obs; + final String tag; + final String keyword; + final SearchType searchType; + + // sort + final RxString order = ''.obs; + late final RxInt duration = 0.obs; int? tids; int? orderSort; int? userType; int? categoryId; - String tag; int? pubBegin; int? pubEnd; - bool? hasJump2Video; SearchResultController? searchResultController; @@ -36,82 +35,28 @@ class SearchPanelController extends CommonListController { try { searchResultController = Get.find(tag: tag); } catch (_) {} - if (searchType == SearchType.video) { - jump2Video(); - } else if (searchType == SearchType.article) { - jump2Article(); - } queryData(); } @override - List? getDataList(response) { + List? getDataList(R response) { return response.list; } @override - bool customHandleResponse(bool isRefresh, Success response) { + bool customHandleResponse(bool isRefresh, Success response) { searchResultController?.count[searchType.index] = - response.response.numResults; - - if (searchType == SearchType.video && hasJump2Video != true && isRefresh) { - hasJump2Video = true; - onPushDetail(response.response.list); - } - + response.response.numResults ?? 0; return false; } - void jump2Video() { - if (RegExp(r'^av\d+$', caseSensitive: false).hasMatch(keyword)) { - hasJump2Video = true; - PiliScheme.videoPush( - int.parse(keyword.substring(2)), - null, - showDialog: false, - ); - } else if (RegExp(r'^bv[a-z\d]{10}$', caseSensitive: false) - .hasMatch(keyword)) { - hasJump2Video = true; - PiliScheme.videoPush(null, keyword, showDialog: false); - } - } - - void jump2Article() { - String? cvid = RegExp(r'^cv(id)?(\d+)$', caseSensitive: false) - .firstMatch(keyword) - ?.group(2); - if (cvid != null) { - WidgetsBinding.instance.addPostFrameCallback((_) { - Get.toNamed( - '/htmlRender', - parameters: { - 'url': 'https://www.bilibili.com/read/cv$cvid', - 'title': '', - 'id': 'cv$cvid', - 'dynamicType': 'read' - }, - ); - }); - } - } - - void onPushDetail(resultList) async { - try { - int? aid = int.tryParse(keyword); - if (aid != null && resultList.first.aid == aid) { - PiliScheme.videoPush(aid, null, showDialog: false); - } - } catch (_) {} - } - @override - Future customGetData() => SearchHttp.searchByType( + Future> customGetData() => SearchHttp.searchByType( searchType: searchType, keyword: keyword, page: currentPage, order: order.value, - duration: searchType.name != 'video' ? null : duration.value, + duration: searchType == SearchType.video ? duration.value : null, tids: tids, orderSort: orderSort, userType: userType, diff --git a/lib/pages/search_panel/index.dart b/lib/pages/search_panel/index.dart deleted file mode 100644 index d1f933245..000000000 --- a/lib/pages/search_panel/index.dart +++ /dev/null @@ -1,4 +0,0 @@ -library searchpanel; - -export './controller.dart'; -export './view.dart'; diff --git a/lib/pages/search_panel/live/view.dart b/lib/pages/search_panel/live/view.dart new file mode 100644 index 000000000..05daed6a6 --- /dev/null +++ b/lib/pages/search_panel/live/view.dart @@ -0,0 +1,62 @@ +import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/models/search/result.dart'; +import 'package:PiliPlus/pages/search_panel/controller.dart'; +import 'package:PiliPlus/pages/search_panel/live/widgets/item.dart'; +import 'package:PiliPlus/pages/search_panel/view.dart'; +import 'package:PiliPlus/utils/grid.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class SearchLivePanel extends CommonSearchPanel { + const SearchLivePanel({ + super.key, + required super.keyword, + required super.tag, + required super.searchType, + }); + + @override + State createState() => _SearchLivePanelState(); +} + +class _SearchLivePanelState extends CommonSearchPanelState { + @override + late final controller = Get.put( + SearchPanelController( + keyword: widget.keyword, + searchType: widget.searchType, + tag: widget.tag, + ), + tag: widget.searchType.name + widget.tag, + ); + + @override + Widget buildList(List list) { + return SliverPadding( + padding: const EdgeInsets.only( + left: StyleString.safeSpace, + right: StyleString.safeSpace, + bottom: 80, + ), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithExtentAndRatio( + maxCrossAxisExtent: Grid.smallCardWidth, + crossAxisSpacing: StyleString.safeSpace, + mainAxisSpacing: StyleString.safeSpace, + childAspectRatio: StyleString.aspectRatio, + mainAxisExtent: MediaQuery.textScalerOf(context).scale(80), + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == list.length - 1) { + controller.onLoadMore(); + } + return LiveItem(liveItem: list[index]); + }, + childCount: list.length, + ), + ), + ); + } +} diff --git a/lib/pages/search_panel/live/widgets/item.dart b/lib/pages/search_panel/live/widgets/item.dart new file mode 100644 index 000000000..8efe0f569 --- /dev/null +++ b/lib/pages/search_panel/live/widgets/item.dart @@ -0,0 +1,140 @@ +import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/image_save.dart'; +import 'package:PiliPlus/common/widgets/network_img_layer.dart'; +import 'package:PiliPlus/models/search/result.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class LiveItem extends StatelessWidget { + final SearchLiveItemModel liveItem; + + const LiveItem({super.key, required this.liveItem}); + + @override + Widget build(BuildContext context) { + return Card( + elevation: 1, + clipBehavior: Clip.hardEdge, + margin: EdgeInsets.zero, + child: InkWell( + onTap: () async { + Get.toNamed('/liveRoom?roomid=${liveItem.roomid}'); + }, + onLongPress: () => imageSaveDialog( + context: context, + title: liveItem.title?.map((item) => item['text']).join() ?? '', + cover: liveItem.cover, + ), + child: Column( + children: [ + ClipRRect( + borderRadius: 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: [ + NetworkImgLayer( + src: liveItem.cover, + type: 'emote', + width: maxWidth, + height: maxHeight, + ), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: AnimatedOpacity( + opacity: 1, + duration: const Duration(milliseconds: 200), + child: liveStat( + liveItem.online, + liveItem.cateName, + ), + ), + ), + ], + ); + }), + ), + ), + liveContent(context) + ], + ), + ), + ); + } + + Widget liveContent(BuildContext context) => Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(9, 8, 9, 6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text.rich( + TextSpan( + children: [ + for (var i in liveItem.title!) ...[ + TextSpan( + text: i['text'], + style: TextStyle( + letterSpacing: 0.3, + color: i['type'] == 'em' + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurface, + ), + ), + ] + ], + ), + ), + SizedBox( + width: double.infinity, + child: Text( + liveItem.uname!, + maxLines: 1, + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline, + ), + ), + ), + ], + ), + ), + ); + + Widget liveStat(int? online, String? cateName) { + return Container( + height: 45, + padding: const EdgeInsets.only(top: 22, left: 8, right: 8), + 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( + cateName!, + style: const TextStyle(fontSize: 11, color: Colors.white), + ), + Text( + '围观:${online.toString()}', + style: const TextStyle(fontSize: 11, color: Colors.white), + ) + ], + ), + ); + } +} diff --git a/lib/pages/search_panel/pgc/view.dart b/lib/pages/search_panel/pgc/view.dart new file mode 100644 index 000000000..64e26a22b --- /dev/null +++ b/lib/pages/search_panel/pgc/view.dart @@ -0,0 +1,159 @@ +import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/badge.dart'; +import 'package:PiliPlus/common/widgets/image_save.dart'; +import 'package:PiliPlus/common/widgets/network_img_layer.dart'; +import 'package:PiliPlus/models/search/result.dart'; +import 'package:PiliPlus/pages/search_panel/controller.dart'; +import 'package:PiliPlus/pages/search_panel/view.dart'; +import 'package:PiliPlus/utils/grid.dart'; +import 'package:PiliPlus/utils/page_utils.dart'; +import 'package:PiliPlus/utils/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class SearchPgcPanel extends CommonSearchPanel { + const SearchPgcPanel({ + super.key, + required super.keyword, + required super.tag, + required super.searchType, + }); + + @override + State createState() => _SearchPgcPanelState(); +} + +class _SearchPgcPanelState extends CommonSearchPanelState { + @override + late final controller = Get.put( + SearchPanelController( + keyword: widget.keyword, + searchType: widget.searchType, + tag: widget.tag, + ), + tag: widget.searchType.name + widget.tag, + ); + + late final TextStyle style = TextStyle(fontSize: 13); + + @override + Widget buildList(List list) { + return SliverPadding( + padding: const EdgeInsets.only(bottom: 80), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: Grid.smallCardWidth * 2, + mainAxisExtent: 160, + ), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + if (index == list.length - 1) { + controller.onLoadMore(); + } + final i = list[index]; + return InkWell( + onTap: () { + PageUtils.viewBangumi(seasonId: i.seasonId); + }, + onLongPress: () => imageSaveDialog( + context: context, + title: i.title?.map((item) => item['text']).join() ?? '', + cover: i.cover, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: StyleString.safeSpace, + vertical: StyleString.cardSpace, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + NetworkImgLayer( + width: 111, + height: 148, + src: i.cover, + ), + PBadge( + text: i.seasonTypeName, + top: 6.0, + right: 4.0, + bottom: null, + left: null, + ) + ], + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + Text.rich( + TextSpan( + style: TextStyle( + color: + Theme.of(context).colorScheme.onSurface), + children: [ + for (var i in i.title!) ...[ + TextSpan( + text: i['text'], + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .titleSmall! + .fontSize!, + fontWeight: FontWeight.bold, + color: i['type'] == 'em' + ? Theme.of(context) + .colorScheme + .primary + : Theme.of(context) + .colorScheme + .onSurface, + ), + ), + ], + ], + ), + ), + const SizedBox(height: 12), + Text('评分:${i.mediaScore?['score']}', style: style), + Row( + children: [ + if (i.areas?.isNotEmpty == true) + Text(i.areas!, style: style), + const SizedBox(width: 3), + const Text('·'), + const SizedBox(width: 3), + Text(Utils.dateFormat(i.pubtime).toString(), + style: style), + ], + ), + Row( + children: [ + if (i.styles?.isNotEmpty == true) + Text(i.styles!, style: style), + const SizedBox(width: 3), + const Text('·'), + const SizedBox(width: 3), + if (i.indexShow?.isNotEmpty == true) + Text(i.indexShow!, style: style), + ], + ), + ], + ), + ), + ], + ), + ), + ); + }, + childCount: list.length, + ), + ), + ); + } +} diff --git a/lib/pages/search_panel/user/controller.dart b/lib/pages/search_panel/user/controller.dart new file mode 100644 index 000000000..c271d0d3e --- /dev/null +++ b/lib/pages/search_panel/user/controller.dart @@ -0,0 +1,126 @@ +import 'dart:math'; + +import 'package:PiliPlus/models/search/result.dart'; +import 'package:PiliPlus/pages/search/widgets/search_text.dart'; +import 'package:PiliPlus/pages/search_panel/controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class SearchUserController + extends SearchPanelController { + SearchUserController({ + required super.keyword, + required super.searchType, + required super.tag, + }); + + // sort + late final List orderFiltersList = [ + {'label': '默认排序', 'value': 0, 'orderSort': 0, 'order': ''}, + {'label': '粉丝数由高到低', 'value': 1, 'orderSort': 0, 'order': 'fans'}, + {'label': '粉丝数由低到高', 'value': 2, 'orderSort': 1, 'order': 'fans'}, + {'label': 'Lv等级由高到低', 'value': 3, 'orderSort': 0, 'order': 'level'}, + {'label': 'Lv等级由低到高', 'value': 4, 'orderSort': 1, 'order': 'level'}, + ]; + late final List userTypeFiltersList = [ + {'label': '全部用户', 'value': 0, 'userType': 0}, + {'label': 'UP主', 'value': 1, 'userType': 1}, + {'label': '普通用户', 'value': 2, 'userType': 2}, + {'label': '认证用户', 'value': 3, 'userType': 3}, + ]; + RxInt currentOrderFilterval = 0.obs; + RxInt currentUserTypeFilterval = 0.obs; + + onShowFilterDialog(BuildContext context) { + showModalBottomSheet( + context: context, + useSafeArea: true, + isScrollControlled: true, + clipBehavior: Clip.hardEdge, + backgroundColor: Theme.of(context).colorScheme.surface, + constraints: BoxConstraints( + maxWidth: min(640, min(Get.width, Get.height)), + ), + builder: (context) => SingleChildScrollView( + child: Container( + width: double.infinity, + padding: EdgeInsets.only( + top: 20, + left: 16, + right: 16, + bottom: 80 + MediaQuery.of(context).padding.bottom, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + const Text('用户粉丝数及等级排序顺序', style: TextStyle(fontSize: 16)), + const SizedBox(height: 10), + Wrap( + spacing: 8, + runSpacing: 8, + children: orderFiltersList + .map( + (item) => SearchText( + text: item['label'], + onTap: (_) async { + Get.back(); + currentOrderFilterval.value = item['value']; + SmartDialog.dismiss(); + SmartDialog.showToast("「${item['label']}」的筛选结果"); + orderSort = item['orderSort']; + order.value = item['order']; + SmartDialog.showLoading(msg: 'loading'); + await onReload(); + SmartDialog.dismiss(); + }, + bgColor: item['value'] == currentOrderFilterval.value + ? Theme.of(context).colorScheme.secondaryContainer + : null, + textColor: item['value'] == currentOrderFilterval.value + ? Theme.of(context).colorScheme.onSecondaryContainer + : null, + ), + ) + .toList(), + ), + const SizedBox(height: 20), + const Text('用户分类', style: TextStyle(fontSize: 16)), + const SizedBox(height: 10), + Wrap( + spacing: 8, + runSpacing: 8, + children: userTypeFiltersList + .map( + (item) => SearchText( + text: item['label'], + onTap: (_) async { + Get.back(); + currentUserTypeFilterval.value = item['value']; + SmartDialog.dismiss(); + SmartDialog.showToast("「${item['label']}」的筛选结果"); + userType = item['userType']; + SmartDialog.showLoading(msg: 'loading'); + await onReload(); + SmartDialog.dismiss(); + }, + bgColor: item['value'] == currentUserTypeFilterval.value + ? Theme.of(context).colorScheme.secondaryContainer + : null, + textColor: item['value'] == + currentUserTypeFilterval.value + ? Theme.of(context).colorScheme.onSecondaryContainer + : null, + ), + ) + .toList(), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/search_panel/user/view.dart b/lib/pages/search_panel/user/view.dart new file mode 100644 index 000000000..c0265abc1 --- /dev/null +++ b/lib/pages/search_panel/user/view.dart @@ -0,0 +1,202 @@ +import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; +import 'package:PiliPlus/common/widgets/network_img_layer.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/search/result.dart'; +import 'package:PiliPlus/pages/search_panel/user/controller.dart'; +import 'package:PiliPlus/pages/search_panel/view.dart'; +import 'package:PiliPlus/utils/grid.dart'; +import 'package:PiliPlus/utils/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class SearchUserPanel extends CommonSearchPanel { + const SearchUserPanel({ + super.key, + required super.keyword, + required super.tag, + required super.searchType, + super.hasHeader = true, + }); + + @override + State createState() => _SearchUserPanelState(); +} + +class _SearchUserPanelState extends CommonSearchPanelState { + @override + late final SearchUserController controller = Get.put( + SearchUserController( + keyword: widget.keyword, + searchType: widget.searchType, + tag: widget.tag, + ), + tag: widget.searchType.name + widget.tag, + ); + + late TextStyle style; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + style = TextStyle( + fontSize: Theme.of(context).textTheme.labelSmall!.fontSize, + color: Theme.of(context).colorScheme.outline, + ); + } + + @override + Widget buildHeader(LoadingState?> loadingState) { + if (loadingState is Success) { + return SliverPersistentHeader( + pinned: false, + floating: true, + delegate: CustomSliverPersistentHeaderDelegate( + extent: 40, + bgColor: Theme.of(context).colorScheme.surface, + child: Container( + height: 40, + padding: const EdgeInsets.only(left: 25, right: 12), + child: Row( + children: [ + Obx( + () => Text( + '排序: ${controller.orderFiltersList[controller.currentOrderFilterval.value]['label']}', + maxLines: 1, + style: + TextStyle(color: Theme.of(context).colorScheme.outline), + ), + ), + const Spacer(), + Obx( + () => Text( + '用户类型: ${controller.userTypeFiltersList[controller.currentUserTypeFilterval.value]['label']}', + maxLines: 1, + style: + TextStyle(color: Theme.of(context).colorScheme.outline), + ), + ), + const Spacer(), + SizedBox( + width: 32, + height: 32, + child: IconButton( + tooltip: '筛选', + style: ButtonStyle( + padding: WidgetStateProperty.all(EdgeInsets.zero), + ), + onPressed: () { + controller.onShowFilterDialog(context); + }, + icon: Icon( + Icons.filter_list_outlined, + size: 18, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ], + ), + ), + ), + ); + } + return const SliverToBoxAdapter(); + } + + @override + Widget buildList(List list) { + return SliverPadding( + padding: const EdgeInsets.only(bottom: 80), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: Grid.smallCardWidth * 2, + mainAxisExtent: 66, + ), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + if (index == list.length - 1) { + controller.onLoadMore(); + } + final item = list[index]; + String heroTag = Utils.makeHeroTag(item.mid); + return InkWell( + onTap: () => Get.toNamed('/member?mid=${item.mid}', + arguments: {'heroTag': heroTag, 'face': item.upic}), + child: Row( + children: [ + const SizedBox(width: 15), + Stack( + clipBehavior: Clip.none, + children: [ + NetworkImgLayer( + width: 42, + height: 42, + src: item.upic, + type: 'avatar', + ), + if (item.officialVerify?['type'] == 0 || + item.officialVerify?['type'] == 1) + Positioned( + bottom: 0, + right: 0, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.surface, + ), + child: Icon( + Icons.offline_bolt, + color: item.officialVerify?['type'] == 0 + ? Colors.yellow + : Colors.lightBlueAccent, + size: 14, + ), + ), + ), + ], + ), + const SizedBox(width: 10), + Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + Text( + item.uname!, + style: const TextStyle( + fontSize: 14, + ), + ), + const SizedBox(width: 6), + Image.asset( + 'assets/images/lv/lv${item.isSeniorMember == 1 ? '6_s' : item.level}.png', + height: 11, + semanticLabel: '等级${item.level}', + ), + ], + ), + Text( + '粉丝:${Utils.numFormat(item.fans)} 视频:${Utils.numFormat(item.videos)}', + style: style, + ), + if (item.officialVerify?['desc'] != null && + item.officialVerify?['desc'] != '') + Text( + item.officialVerify?['desc'], + style: style, + ), + ], + ) + ], + ), + ); + }, + childCount: list.length, + ), + ), + ); + } +} diff --git a/lib/pages/search_panel/widgets/video_panel.dart b/lib/pages/search_panel/video/controller.dart similarity index 55% rename from lib/pages/search_panel/widgets/video_panel.dart rename to lib/pages/search_panel/video/controller.dart index 96b4ec4d9..1a48acb0a 100644 --- a/lib/pages/search_panel/widgets/video_panel.dart +++ b/lib/pages/search_panel/video/controller.dart @@ -1,146 +1,114 @@ import 'dart:math'; -import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; -import 'package:PiliPlus/common/widgets/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/common/search_type.dart'; +import 'package:PiliPlus/models/search/result.dart'; import 'package:PiliPlus/pages/search/widgets/search_text.dart'; +import 'package:PiliPlus/pages/search_panel/controller.dart'; +import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; -import 'package:PiliPlus/common/widgets/video_card_h.dart'; -import 'package:PiliPlus/models/common/search_type.dart'; -import 'package:PiliPlus/pages/search_panel/index.dart'; import 'package:intl/intl.dart'; -import '../../../utils/grid.dart'; +class SearchVideoController + extends SearchPanelController { + SearchVideoController({ + required super.keyword, + required super.searchType, + required super.tag, + }); -Widget searchVideoPanel( - BuildContext context, - SearchPanelController searchPanelCtr, - LoadingState?> loadingState) { - final controller = Get.put(VideoPanelController(), tag: searchPanelCtr.tag); - return CustomScrollView( - controller: searchPanelCtr.scrollController, - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - SliverPersistentHeader( - pinned: false, - floating: true, - delegate: CustomSliverPersistentHeaderDelegate( - extent: 34, - bgColor: Theme.of(context).colorScheme.surface, - child: Container( - height: 34, - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row( - children: [ - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Obx( - () => Wrap( - // spacing: , - children: [ - for (var i in controller.filterList) ...[ - SearchText( - fontSize: 13, - text: i['label'], - bgColor: Colors.transparent, - textColor: - controller.selectedType.value == i['type'] - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.outline, - onTap: (value) async { - controller.selectedType.value = i['type']; - searchPanelCtr.order.value = - i['type'].toString().split('.').last; - SmartDialog.showLoading(msg: 'loading'); - await searchPanelCtr.onReload(); - SmartDialog.dismiss(); - }, - ), - ] - ], - ), - ), - ), - ), - const VerticalDivider(indent: 7, endIndent: 8), - const SizedBox(width: 3), - SizedBox( - width: 32, - height: 32, - child: IconButton( - tooltip: '筛选', - style: ButtonStyle( - padding: WidgetStateProperty.all(EdgeInsets.zero), - ), - onPressed: () => - controller.onShowFilterDialog(context, searchPanelCtr), - icon: Icon( - Icons.filter_list_outlined, - size: 18, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - ], - ), - ), - ), - ), - switch (loadingState) { - Success() => loadingState.response?.isNotEmpty == true - ? SliverPadding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom + 80, - ), - sliver: SliverGrid( - gridDelegate: Grid.videoCardHDelegate(context), - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - if (index == loadingState.response!.length - 1) { - searchPanelCtr.onLoadMore(); - } - return VideoCardH( - videoItem: loadingState.response![index], - showPubdate: true, - ); - }, - childCount: loadingState.response!.length, - ), - ), - ) - : HttpError( - callback: searchPanelCtr.onReload, - ), - Error() => HttpError( - errMsg: loadingState.errMsg, - callback: searchPanelCtr.onReload, - ), - _ => throw UnimplementedError(), - }, - ], - ); -} + bool? hasJump2Video; -class VideoPanelController extends GetxController { - RxList filterList = [{}].obs; - Rx selectedType = ArchiveFilterType.values.first.obs; - List pubTimeFiltersList = [ + @override + void onInit() { + super.onInit(); + DateTime now = DateTime.now(); + pubBeginDate = DateTime( + now.year, + now.month, + 1, + 0, + 0, + 0, + ); + pubEndDate = DateTime( + now.year, + now.month, + now.day, + 23, + 59, + 59, + ); + + jump2Video(); + } + + @override + List? getDataList(SearchVideoModel response) { + return response.list; + } + + @override + bool customHandleResponse( + bool isRefresh, Success response) { + searchResultController?.count[searchType.index] = + response.response.numResults ?? 0; + if (searchType == SearchType.video && hasJump2Video != true && isRefresh) { + hasJump2Video = true; + onPushDetail(response.response.list); + } + return false; + } + + void onPushDetail(resultList) async { + try { + int? aid = int.tryParse(keyword); + if (aid != null && resultList.first.aid == aid) { + PiliScheme.videoPush(aid, null, showDialog: false); + } + } catch (_) {} + } + + void jump2Video() { + if (RegExp(r'^av\d+$', caseSensitive: false).hasMatch(keyword)) { + hasJump2Video = true; + PiliScheme.videoPush( + int.parse(keyword.substring(2)), + null, + showDialog: false, + ); + } else if (RegExp(r'^bv[a-z\d]{10}$', caseSensitive: false) + .hasMatch(keyword)) { + hasJump2Video = true; + PiliScheme.videoPush(null, keyword, showDialog: false); + } + } + + // sort + late final List filterList = ArchiveFilterType.values + .map((type) => { + 'label': type.description, + 'type': type, + }) + .toList(); + late final Rx selectedType = + ArchiveFilterType.values.first.obs; + late final List pubTimeFiltersList = [ {'label': '不限', 'value': 0}, {'label': '最近一天', 'value': 1}, {'label': '最近一周', 'value': 2}, {'label': '最近半年', 'value': 3}, ]; - List timeFiltersList = [ + late final List timeFiltersList = [ {'label': '全部时长', 'value': 0}, {'label': '0-10分钟', 'value': 1}, {'label': '10-30分钟', 'value': 2}, {'label': '30-60分钟', 'value': 3}, {'label': '60分钟+', 'value': 4}, ]; - List zoneFiltersList = [ + late final List zoneFiltersList = [ {'label': '全部', 'value': 0}, {'label': '动画', 'value': 1, 'tids': 1}, {'label': '番剧', 'value': 2, 'tids': 13}, @@ -164,47 +132,15 @@ class VideoPanelController extends GetxController { {'label': '电影', 'value': 20, 'tids': 23}, {'label': '电视', 'value': 21, 'tids': 11}, ]; - int currentPubTimeFilterval = 0; - late DateTime pubBegin; - late DateTime pubEnd; - bool customPubBegin = false; - bool customPubEnd = false; - int currentTimeFilterval = 0; - int currentZoneFilterval = 0; + int currentPubTimeFilter = 0; + late DateTime pubBeginDate; + late DateTime pubEndDate; + bool customPubBeginDate = false; + bool customPubEndDate = false; + int currentTimeFilter = 0; + int currentZoneFilter = 0; - @override - void onInit() { - DateTime now = DateTime.now(); - pubBegin = DateTime( - now.year, - now.month, - 1, - 0, - 0, - 0, - ); - pubEnd = DateTime( - now.year, - now.month, - now.day, - 23, - 59, - 59, - ); - List> list = ArchiveFilterType.values - .map((type) => { - 'label': type.description, - 'type': type, - }) - .toList(); - filterList.value = list; - super.onInit(); - } - - onShowFilterDialog( - BuildContext context, - SearchPanelController searchPanelCtr, - ) { + onShowFilterDialog(BuildContext context) { showModalBottomSheet( context: context, useSafeArea: true, @@ -218,43 +154,39 @@ class VideoPanelController extends GetxController { builder: (context, setState) { Widget dateWidget([bool isFirst = true]) { return SearchText( - text: - DateFormat('yyyy-MM-dd').format(isFirst ? pubBegin : pubEnd), + text: DateFormat('yyyy-MM-dd') + .format(isFirst ? pubBeginDate : pubEndDate), textAlign: TextAlign.center, onTap: (text) { showDatePicker( context: context, - initialDate: isFirst ? pubBegin : pubEnd, - firstDate: isFirst ? DateTime(2009, 6, 26) : pubBegin, - lastDate: isFirst ? pubEnd : DateTime.now(), + initialDate: isFirst ? pubBeginDate : pubEndDate, + firstDate: isFirst ? DateTime(2009, 6, 26) : pubBeginDate, + lastDate: isFirst ? pubEndDate : DateTime.now(), ).then((selectedDate) async { if (selectedDate != null) { if (isFirst) { - customPubBegin = true; - pubBegin = selectedDate; + customPubBeginDate = true; + pubBeginDate = selectedDate; } else { - customPubEnd = true; - pubEnd = selectedDate; + customPubEndDate = true; + pubEndDate = selectedDate; } - currentPubTimeFilterval = -1; + currentPubTimeFilter = -1; SmartDialog.dismiss(); - // SmartDialog.showToast("「${item['label']}」的筛选结果"); - SearchPanelController ctr = Get.find( - tag: searchPanelCtr.searchType.name + searchPanelCtr.tag, - ); - ctr.pubBegin = DateTime( - pubBegin.year, - pubBegin.month, - pubBegin.day, + pubBegin = DateTime( + pubBeginDate.year, + pubBeginDate.month, + pubBeginDate.day, 0, 0, 0, ).millisecondsSinceEpoch ~/ 1000; - ctr.pubEnd = DateTime( - pubEnd.year, - pubEnd.month, - pubEnd.day, + pubEnd = DateTime( + pubEndDate.year, + pubEndDate.month, + pubEndDate.day, 23, 59, 59, @@ -262,17 +194,17 @@ class VideoPanelController extends GetxController { 1000; setState(() {}); SmartDialog.showLoading(msg: 'loading'); - await ctr.onReload(); + await onReload(); SmartDialog.dismiss(); } }); }, - bgColor: currentPubTimeFilterval == -1 && - (isFirst ? customPubBegin : customPubEnd) + bgColor: currentPubTimeFilter == -1 && + (isFirst ? customPubBeginDate : customPubEndDate) ? Theme.of(context).colorScheme.secondaryContainer : Theme.of(context).colorScheme.outline.withOpacity(0.1), - textColor: currentPubTimeFilterval == -1 && - (isFirst ? customPubBegin : customPubEnd) + textColor: currentPubTimeFilter == -1 && + (isFirst ? customPubBeginDate : customPubEndDate) ? Theme.of(context).colorScheme.onSecondaryContainer : Theme.of(context).colorScheme.outline.withOpacity(0.8), ); @@ -303,20 +235,15 @@ class VideoPanelController extends GetxController { text: item['label'], onTap: (text) async { Get.back(); - currentPubTimeFilterval = item['value']; + currentPubTimeFilter = item['value']; SmartDialog.dismiss(); SmartDialog.showToast("「${item['label']}」的筛选结果"); - SearchPanelController ctr = - Get.find( - tag: searchPanelCtr.searchType.name + - searchPanelCtr.tag, - ); DateTime now = DateTime.now(); if (item['value'] == 0) { - ctr.pubBegin = null; - ctr.pubEnd = null; + pubBegin = null; + pubEnd = null; } else { - ctr.pubBegin = DateTime( + pubBegin = DateTime( now.year, now.month, now.day - @@ -330,7 +257,7 @@ class VideoPanelController extends GetxController { 0, ).millisecondsSinceEpoch ~/ 1000; - ctr.pubEnd = DateTime( + pubEnd = DateTime( now.year, now.month, now.day, @@ -341,15 +268,15 @@ class VideoPanelController extends GetxController { 1000; } SmartDialog.showLoading(msg: 'loading'); - await ctr.onReload(); + await onReload(); SmartDialog.dismiss(); }, - bgColor: item['value'] == currentPubTimeFilterval + bgColor: item['value'] == currentPubTimeFilter ? Theme.of(context) .colorScheme .secondaryContainer : null, - textColor: item['value'] == currentPubTimeFilterval + textColor: item['value'] == currentPubTimeFilter ? Theme.of(context) .colorScheme .onSecondaryContainer @@ -383,25 +310,20 @@ class VideoPanelController extends GetxController { text: item['label'], onTap: (text) async { Get.back(); - currentTimeFilterval = item['value']; + currentTimeFilter = item['value']; SmartDialog.dismiss(); SmartDialog.showToast("「${item['label']}」的筛选结果"); - SearchPanelController ctr = - Get.find( - tag: searchPanelCtr.searchType.name + - searchPanelCtr.tag, - ); - ctr.duration.value = item['value']; + duration.value = item['value']; SmartDialog.showLoading(msg: 'loading'); - await ctr.onReload(); + await onReload(); SmartDialog.dismiss(); }, - bgColor: item['value'] == currentTimeFilterval + bgColor: item['value'] == currentTimeFilter ? Theme.of(context) .colorScheme .secondaryContainer : null, - textColor: item['value'] == currentTimeFilterval + textColor: item['value'] == currentTimeFilter ? Theme.of(context) .colorScheme .onSecondaryContainer @@ -422,25 +344,20 @@ class VideoPanelController extends GetxController { text: item['label'], onTap: (text) async { Get.back(); - currentZoneFilterval = item['value']; + currentZoneFilter = item['value']; SmartDialog.dismiss(); SmartDialog.showToast("「${item['label']}」的筛选结果"); - SearchPanelController ctr = - Get.find( - tag: searchPanelCtr.searchType.name + - searchPanelCtr.tag, - ); - ctr.tids = item['tids']; + tids = item['tids']; SmartDialog.showLoading(msg: 'loading'); - await ctr.onReload(); + await onReload(); SmartDialog.dismiss(); }, - bgColor: item['value'] == currentZoneFilterval + bgColor: item['value'] == currentZoneFilter ? Theme.of(context) .colorScheme .secondaryContainer : null, - textColor: item['value'] == currentZoneFilterval + textColor: item['value'] == currentZoneFilter ? Theme.of(context) .colorScheme .onSecondaryContainer diff --git a/lib/pages/search_panel/video/view.dart b/lib/pages/search_panel/video/view.dart new file mode 100644 index 000000000..627d31b54 --- /dev/null +++ b/lib/pages/search_panel/video/view.dart @@ -0,0 +1,130 @@ +import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; +import 'package:PiliPlus/common/widgets/video_card_h.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/search/result.dart'; +import 'package:PiliPlus/pages/search/widgets/search_text.dart'; +import 'package:PiliPlus/pages/search_panel/video/controller.dart'; +import 'package:PiliPlus/pages/search_panel/view.dart'; +import 'package:PiliPlus/utils/grid.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class SearchVideoPanel extends CommonSearchPanel { + const SearchVideoPanel({ + super.key, + required super.keyword, + required super.tag, + required super.searchType, + super.hasHeader = true, + }); + + @override + State createState() => _SearchVideoPanelState(); +} + +class _SearchVideoPanelState extends CommonSearchPanelState { + @override + late final SearchVideoController controller = Get.put( + SearchVideoController( + keyword: widget.keyword, + searchType: widget.searchType, + tag: widget.tag, + ), + tag: widget.searchType.name + widget.tag, + ); + + @override + Widget buildHeader(LoadingState?> loadingState) { + if (loadingState is Success) { + return SliverPersistentHeader( + pinned: false, + floating: true, + delegate: CustomSliverPersistentHeaderDelegate( + extent: 34, + bgColor: Theme.of(context).colorScheme.surface, + child: Container( + height: 34, + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Wrap( + children: [ + for (var i in controller.filterList) ...[ + Obx( + () => SearchText( + fontSize: 13, + text: i['label'], + bgColor: Colors.transparent, + textColor: + controller.selectedType.value == i['type'] + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.outline, + onTap: (value) async { + controller.selectedType.value = i['type']; + controller.order.value = + i['type'].toString().split('.').last; + SmartDialog.showLoading(msg: 'loading'); + await controller.onReload(); + SmartDialog.dismiss(); + }, + ), + ), + ] + ], + ), + ), + ), + const VerticalDivider(indent: 7, endIndent: 8), + const SizedBox(width: 3), + SizedBox( + width: 32, + height: 32, + child: IconButton( + tooltip: '筛选', + style: ButtonStyle( + padding: WidgetStateProperty.all(EdgeInsets.zero), + ), + onPressed: () => controller.onShowFilterDialog(context), + icon: Icon( + Icons.filter_list_outlined, + size: 18, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ], + ), + ), + ), + ); + } + return const SliverToBoxAdapter(); + } + + @override + Widget buildList(List list) { + return SliverPadding( + padding: const EdgeInsets.only(bottom: 80), + sliver: SliverGrid( + gridDelegate: Grid.videoCardHDelegate(context), + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == list.length - 1) { + controller.onLoadMore(); + } + return VideoCardH( + videoItem: list[index], + showPubdate: true, + ); + }, + childCount: list.length, + ), + ), + ); + } +} diff --git a/lib/pages/search_panel/view.dart b/lib/pages/search_panel/view.dart index cbfec44bb..d4e144dec 100644 --- a/lib/pages/search_panel/view.dart +++ b/lib/pages/search_panel/view.dart @@ -1,134 +1,106 @@ -import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; -import 'package:PiliPlus/http/loading_state.dart'; -import 'package:PiliPlus/pages/search_panel/widgets/video_panel.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/skeleton/media_bangumi.dart'; import 'package:PiliPlus/common/skeleton/video_card_h.dart'; +import 'package:PiliPlus/common/widgets/http_error.dart'; +import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/search/result.dart'; +import 'package:PiliPlus/utils/grid.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:PiliPlus/models/common/search_type.dart'; -import '../../common/constants.dart'; -import '../../utils/grid.dart'; import 'controller.dart'; -import 'widgets/article_panel.dart'; -import 'widgets/live_panel.dart'; -import 'widgets/media_bangumi_panel.dart'; -import 'widgets/user_panel.dart'; -class SearchPanel extends StatefulWidget { - final String keyword; - final SearchType searchType; - final String tag; - const SearchPanel({ +abstract class CommonSearchPanel extends StatefulWidget { + const CommonSearchPanel({ super.key, required this.keyword, required this.searchType, required this.tag, + this.hasHeader = false, }); - @override - State createState() => _SearchPanelState(); + final String keyword; + final SearchType searchType; + final String tag; + final bool hasHeader; } -class _SearchPanelState extends State - with AutomaticKeepAliveClientMixin { - late SearchPanelController _searchPanelController; +abstract class CommonSearchPanelState< + S extends CommonSearchPanel, + R extends SearchNumData, + T> extends State with AutomaticKeepAliveClientMixin { + SearchPanelController get controller; @override bool get wantKeepAlive => true; - @override - void initState() { - super.initState(); - _searchPanelController = Get.put( - SearchPanelController( - keyword: widget.keyword, - searchType: widget.searchType, - tag: widget.tag, - ), - tag: widget.searchType.name + widget.tag, - ); - } - @override Widget build(BuildContext context) { super.build(context); return refreshIndicator( onRefresh: () async { - await _searchPanelController.onRefresh(); + await controller.onRefresh(); }, - child: Obx(() => _buildBody(_searchPanelController.loadingState.value)), + child: SafeArea( + bottom: false, + child: CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + if (widget.hasHeader) + Obx(() => buildHeader(controller.loadingState.value)), + Obx(() => _buildBody(controller.loadingState.value)), + ], + ), + ), ); } - Widget _buildBody(LoadingState?> loadingState) { - if (loadingState is Loading) { - return CustomScrollView( - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - SliverGrid( - gridDelegate: widget.searchType == SearchType.media_bangumi || - widget.searchType == SearchType.media_ft - ? SliverGridDelegateWithExtentAndRatio( - mainAxisSpacing: 2, - maxCrossAxisExtent: Grid.smallCardWidth * 2, - childAspectRatio: StyleString.aspectRatio * 1.5, - minHeight: MediaQuery.textScalerOf(context).scale(155), - ) - : Grid.videoCardHDelegate(context), - delegate: SliverChildBuilderDelegate( - (context, index) { - switch (widget.searchType) { - case SearchType.video: - return const VideoCardHSkeleton(); - case SearchType.media_bangumi || SearchType.media_ft: - return const MediaBangumiSkeleton(); - case SearchType.bili_user: - return const VideoCardHSkeleton(); - case SearchType.live_room: - return const VideoCardHSkeleton(); - default: - return const VideoCardHSkeleton(); - } - }, - childCount: 15, - ), - ), - ], - ); - } else { - switch (widget.searchType) { - case SearchType.video: - return searchVideoPanel( - context, - _searchPanelController, - loadingState, - ); - case SearchType.media_bangumi || SearchType.media_ft: - return searchBangumiPanel( - context, - _searchPanelController, - loadingState, - ); - case SearchType.bili_user: - return searchUserPanel( - context, - _searchPanelController, - loadingState, - ); - case SearchType.live_room: - return searchLivePanel( - context, - _searchPanelController, - loadingState, - ); - case SearchType.article: - return searchArticlePanel( - context, - _searchPanelController, - loadingState, - ); - } - } + Widget buildHeader(LoadingState?> loadingState) { + throw UnimplementedError(); } + + Widget get _builLoading { + return SliverGrid( + gridDelegate: widget.searchType == SearchType.media_bangumi || + widget.searchType == SearchType.media_ft + ? SliverGridDelegateWithExtentAndRatio( + mainAxisSpacing: 2, + maxCrossAxisExtent: Grid.smallCardWidth * 2, + childAspectRatio: StyleString.aspectRatio * 1.5, + minHeight: MediaQuery.textScalerOf(context).scale(155), + ) + : Grid.videoCardHDelegate(context), + delegate: SliverChildBuilderDelegate( + (context, index) { + switch (widget.searchType) { + case SearchType.media_bangumi || SearchType.media_ft: + return const MediaBangumiSkeleton(); + default: + return const VideoCardHSkeleton(); + } + }, + childCount: 15, + ), + ); + } + + Widget _buildBody(LoadingState?> loadingState) { + return switch (loadingState) { + Loading() => _builLoading, + Success() => loadingState.response?.isNotEmpty == true + ? buildList(loadingState.response!) + : HttpError( + callback: controller.onReload, + ), + Error() => HttpError( + errMsg: loadingState.errMsg, + callback: controller.onReload, + ), + _ => throw UnimplementedError(), + }; + } + + Widget buildList(List list); } diff --git a/lib/pages/search_panel/widgets/article_panel.dart b/lib/pages/search_panel/widgets/article_panel.dart deleted file mode 100644 index af9d15b2b..000000000 --- a/lib/pages/search_panel/widgets/article_panel.dart +++ /dev/null @@ -1,346 +0,0 @@ -import 'dart:math'; - -import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; -import 'package:PiliPlus/common/widgets/http_error.dart'; -import 'package:PiliPlus/common/widgets/image_save.dart'; -import 'package:PiliPlus/http/loading_state.dart'; -import 'package:PiliPlus/pages/search/widgets/search_text.dart'; -import 'package:PiliPlus/pages/search_panel/controller.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:get/get.dart'; -import 'package:PiliPlus/common/constants.dart'; -import 'package:PiliPlus/common/widgets/network_img_layer.dart'; -import 'package:PiliPlus/utils/utils.dart'; - -import '../../../utils/grid.dart'; - -Widget searchArticlePanel( - BuildContext context, - SearchPanelController searchPanelCtr, - LoadingState?> loadingState) { - TextStyle textStyle = TextStyle( - fontSize: Theme.of(context).textTheme.labelSmall!.fontSize, - color: Theme.of(context).colorScheme.outline); - final ctr = Get.put(ArticlePanelController(), tag: searchPanelCtr.tag); - - return CustomScrollView( - controller: searchPanelCtr.scrollController, - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - SliverPersistentHeader( - pinned: false, - floating: true, - delegate: CustomSliverPersistentHeaderDelegate( - extent: 40, - bgColor: Theme.of(context).colorScheme.surface, - child: Container( - height: 40, - padding: const EdgeInsets.only(left: 25, right: 12), - child: Row( - children: [ - Obx( - () => Text( - '排序: ${ctr.orderFiltersList[ctr.currentOrderFilterval.value]['label']}', - maxLines: 1, - style: - TextStyle(color: Theme.of(context).colorScheme.outline), - ), - ), - const Spacer(), - Obx( - () => Text( - '分区: ${ctr.zoneFiltersList[ctr.currentZoneFilterval.value]['label']}', - maxLines: 1, - style: - TextStyle(color: Theme.of(context).colorScheme.outline), - ), - ), - const Spacer(), - SizedBox( - width: 32, - height: 32, - child: IconButton( - tooltip: '筛选', - style: ButtonStyle( - padding: WidgetStateProperty.all(EdgeInsets.zero), - ), - onPressed: () { - ctr.onShowFilterDialog(context, searchPanelCtr); - }, - icon: Icon( - Icons.filter_list_outlined, - size: 18, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - ], - ), - ), - ), - ), - switch (loadingState) { - Success() => loadingState.response?.isNotEmpty == true - ? SliverPadding( - padding: EdgeInsets.only( - bottom: StyleString.safeSpace + - MediaQuery.of(context).padding.bottom, - ), - sliver: SliverGrid( - gridDelegate: Grid.videoCardHDelegate(context), - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - if (index == loadingState.response!.length - 1) { - searchPanelCtr.onLoadMore(); - } - final item = loadingState.response![index]; - return InkWell( - onTap: () { - Get.toNamed('/htmlRender', parameters: { - 'url': 'www.bilibili.com/read/cv${item.id}', - 'title': item.subTitle, - 'id': 'cv${item.id}', - 'dynamicType': 'read' - }); - }, - onLongPress: () => imageSaveDialog( - context: context, - title: (item.title as List?) - ?.map((item) => item['text']) - .join() ?? - '', - cover: item.imageUrls.first, - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: StyleString.safeSpace, - vertical: 5, - ), - child: LayoutBuilder( - builder: (context, boxConstraints) { - final double width = (boxConstraints.maxWidth - - StyleString.cardSpace * - 6 / - MediaQuery.textScalerOf(context) - .scale(1.0)) / - 2; - return Container( - constraints: - const BoxConstraints(minHeight: 88), - height: width / StyleString.aspectRatio, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (item.imageUrls != null && - item.imageUrls.isNotEmpty) - AspectRatio( - aspectRatio: StyleString.aspectRatio, - child: LayoutBuilder( - builder: (context, boxConstraints) { - double maxWidth = - boxConstraints.maxWidth; - double maxHeight = - boxConstraints.maxHeight; - return NetworkImgLayer( - width: maxWidth, - height: maxHeight, - src: item.imageUrls.first, - ); - }), - ), - const SizedBox(width: 10), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text.rich( - maxLines: 2, - TextSpan( - children: [ - for (var i in item.title) ...[ - TextSpan( - text: i['text'], - style: TextStyle( - color: i['type'] == 'em' - ? Theme.of(context) - .colorScheme - .primary - : Theme.of(context) - .colorScheme - .onSurface, - ), - ), - ] - ], - ), - ), - const Spacer(), - Text( - Utils.dateFormat(item.pubTime, - formatType: 'detail'), - style: textStyle), - Row( - children: [ - Text('${item.view}浏览', - style: textStyle), - Text(' • ', style: textStyle), - Text('${item.reply}评论', - style: textStyle), - ], - ), - ], - ), - ), - ], - ), - ); - }, - ), - ), - ); - }, - childCount: loadingState.response!.length, - ), - ), - ) - : HttpError( - callback: searchPanelCtr.onReload, - ), - Error() => HttpError( - errMsg: loadingState.errMsg, - callback: searchPanelCtr.onReload, - ), - LoadingState() => throw UnimplementedError(), - }, - ], - ); -} - -class ArticlePanelController extends GetxController { - List orderFiltersList = [ - {'label': '综合排序', 'value': 0, 'order': 'totalrank'}, - {'label': '最新发布', 'value': 1, 'order': 'pubdate'}, - {'label': '最多点击', 'value': 2, 'order': 'click'}, - {'label': '最多喜欢', 'value': 3, 'order': 'attention'}, - {'label': '最多评论', 'value': 4, 'order': 'scores'}, - ]; - List zoneFiltersList = [ - {'label': '全部分区', 'value': 0, 'categoryId': 0}, - {'label': '动画', 'value': 1, 'categoryId': 2}, - {'label': '游戏', 'value': 2, 'categoryId': 1}, - {'label': '影视', 'value': 3, 'categoryId': 28}, - {'label': '生活', 'value': 4, 'categoryId': 3}, - {'label': '兴趣', 'value': 5, 'categoryId': 29}, - {'label': '轻小说', 'value': 6, 'categoryId': 16}, - {'label': '科技', 'value': 7, 'categoryId': 17}, - {'label': '笔记', 'value': 8, 'categoryId': 41}, - ]; - RxInt currentOrderFilterval = 0.obs; - RxInt currentZoneFilterval = 0.obs; - - onShowFilterDialog( - BuildContext context, - SearchPanelController searchPanelCtr, - ) { - showModalBottomSheet( - context: context, - useSafeArea: true, - isScrollControlled: true, - clipBehavior: Clip.hardEdge, - backgroundColor: Theme.of(context).colorScheme.surface, - constraints: BoxConstraints( - maxWidth: min(640, min(Get.width, Get.height)), - ), - builder: (context) => SingleChildScrollView( - child: Container( - width: double.infinity, - padding: EdgeInsets.only( - top: 20, - left: 16, - right: 16, - bottom: 80 + MediaQuery.of(context).padding.bottom, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 10), - const Text('排序', style: TextStyle(fontSize: 16)), - const SizedBox(height: 10), - Wrap( - spacing: 8, - runSpacing: 8, - children: orderFiltersList - .map( - (item) => SearchText( - text: item['label'], - onTap: (_) async { - Get.back(); - currentOrderFilterval.value = item['value']; - SmartDialog.dismiss(); - SmartDialog.showToast("「${item['label']}」的筛选结果"); - SearchPanelController ctr = - Get.find( - tag: searchPanelCtr.searchType.name + - searchPanelCtr.tag, - ); - ctr.order.value = item['order']; - SmartDialog.showLoading(msg: 'loading'); - await ctr.onReload(); - SmartDialog.dismiss(); - }, - bgColor: item['value'] == currentOrderFilterval.value - ? Theme.of(context).colorScheme.secondaryContainer - : null, - textColor: item['value'] == currentOrderFilterval.value - ? Theme.of(context).colorScheme.onSecondaryContainer - : null, - ), - ) - .toList(), - ), - const SizedBox(height: 20), - const Text('分区', style: TextStyle(fontSize: 16)), - const SizedBox(height: 10), - Wrap( - spacing: 8, - runSpacing: 8, - children: zoneFiltersList - .map( - (item) => SearchText( - text: item['label'], - onTap: (_) async { - Get.back(); - currentZoneFilterval.value = item['value']; - SmartDialog.dismiss(); - SmartDialog.showToast("「${item['label']}」的筛选结果"); - SearchPanelController ctr = - Get.find( - tag: searchPanelCtr.searchType.name + - searchPanelCtr.tag, - ); - ctr.categoryId = item['categoryId']; - SmartDialog.showLoading(msg: 'loading'); - await ctr.onReload(); - SmartDialog.dismiss(); - }, - bgColor: item['value'] == currentZoneFilterval.value - ? Theme.of(context).colorScheme.secondaryContainer - : null, - textColor: item['value'] == currentZoneFilterval.value - ? Theme.of(context).colorScheme.onSecondaryContainer - : null, - ), - ) - .toList(), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/pages/search_panel/widgets/live_panel.dart b/lib/pages/search_panel/widgets/live_panel.dart deleted file mode 100644 index 0035ed518..000000000 --- a/lib/pages/search_panel/widgets/live_panel.dart +++ /dev/null @@ -1,203 +0,0 @@ -import 'package:PiliPlus/common/widgets/image_save.dart'; -import 'package:PiliPlus/common/widgets/loading_widget.dart'; -import 'package:PiliPlus/http/loading_state.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:PiliPlus/common/constants.dart'; -import 'package:PiliPlus/common/widgets/network_img_layer.dart'; - -import '../../../utils/grid.dart'; - -Widget searchLivePanel( - BuildContext context, ctr, LoadingState?> loadingState) { - return switch (loadingState) { - Success() => loadingState.response?.isNotEmpty == true - ? GridView.builder( - physics: const AlwaysScrollableScrollPhysics(), - padding: EdgeInsets.only( - left: StyleString.safeSpace, - right: StyleString.safeSpace, - bottom: MediaQuery.paddingOf(context).bottom + 80, - ), - primary: false, - controller: ctr!.scrollController, - gridDelegate: SliverGridDelegateWithExtentAndRatio( - maxCrossAxisExtent: Grid.smallCardWidth, - crossAxisSpacing: StyleString.safeSpace, - mainAxisSpacing: StyleString.safeSpace, - childAspectRatio: StyleString.aspectRatio, - mainAxisExtent: MediaQuery.textScalerOf(context).scale(80), - ), - itemCount: loadingState.response!.length, - itemBuilder: (context, index) { - if (index == loadingState.response!.length - 1) { - ctr.onLoadMore(); - } - return LiveItem(liveItem: loadingState.response![index]); - }, - ) - : errorWidget( - callback: ctr.onReload, - ), - Error() => errorWidget( - errMsg: loadingState.errMsg, - callback: ctr.onReload, - ), - LoadingState() => throw UnimplementedError(), - }; -} - -class LiveItem extends StatelessWidget { - final dynamic liveItem; - const LiveItem({super.key, required this.liveItem}); - - @override - Widget build(BuildContext context) { - return Card( - elevation: 1, - clipBehavior: Clip.hardEdge, - margin: EdgeInsets.zero, - child: InkWell( - onTap: () async { - Get.toNamed('/liveRoom?roomid=${liveItem.roomid}'); - }, - onLongPress: () => imageSaveDialog( - context: context, - title: - (liveItem.title as List?)?.map((item) => item['text']).join() ?? - '', - cover: liveItem.cover, - ), - child: Column( - children: [ - ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: StyleString.imgRadius, - topRight: StyleString.imgRadius, - bottomLeft: StyleString.imgRadius, - bottomRight: StyleString.imgRadius, - ), - child: AspectRatio( - aspectRatio: StyleString.aspectRatio, - child: LayoutBuilder(builder: (context, boxConstraints) { - double maxWidth = boxConstraints.maxWidth; - double maxHeight = boxConstraints.maxHeight; - return Stack( - children: [ - NetworkImgLayer( - src: liveItem.cover, - type: 'emote', - width: maxWidth, - height: maxHeight, - ), - Positioned( - left: 0, - right: 0, - bottom: 0, - child: AnimatedOpacity( - opacity: 1, - duration: const Duration(milliseconds: 200), - child: LiveStat( - online: liveItem.online, - cateName: liveItem.cateName, - ), - ), - ), - ], - ); - }), - ), - ), - LiveContent(liveItem: liveItem) - ], - ), - ), - ); - } -} - -class LiveContent extends StatelessWidget { - final dynamic liveItem; - const LiveContent({super.key, required this.liveItem}); - @override - Widget build(BuildContext context) { - return Expanded( - child: Padding( - padding: const EdgeInsets.fromLTRB(9, 8, 9, 6), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text.rich( - TextSpan( - children: [ - for (var i in liveItem.title) ...[ - TextSpan( - text: i['text'], - style: TextStyle( - letterSpacing: 0.3, - color: i['type'] == 'em' - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface, - ), - ), - ] - ], - ), - ), - SizedBox( - width: double.infinity, - child: Text( - liveItem.uname, - maxLines: 1, - style: TextStyle( - fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, - color: Theme.of(context).colorScheme.outline, - ), - ), - ), - ], - ), - ), - ); - } -} - -class LiveStat extends StatelessWidget { - final int? online; - final String? cateName; - - const LiveStat({super.key, required this.online, this.cateName}); - - @override - Widget build(BuildContext context) { - return Container( - height: 45, - padding: const EdgeInsets.only(top: 22, left: 8, right: 8), - 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( - cateName!, - style: const TextStyle(fontSize: 11, color: Colors.white), - ), - Text( - '围观:${online.toString()}', - style: const TextStyle(fontSize: 11, color: Colors.white), - ) - ], - ), - ); - } -} diff --git a/lib/pages/search_panel/widgets/media_bangumi_panel.dart b/lib/pages/search_panel/widgets/media_bangumi_panel.dart deleted file mode 100644 index 4313316cf..000000000 --- a/lib/pages/search_panel/widgets/media_bangumi_panel.dart +++ /dev/null @@ -1,158 +0,0 @@ -import 'package:PiliPlus/common/widgets/image_save.dart'; -import 'package:PiliPlus/common/widgets/loading_widget.dart'; -import 'package:PiliPlus/http/loading_state.dart'; -import 'package:PiliPlus/utils/page_utils.dart'; -import 'package:flutter/material.dart'; -import 'package:PiliPlus/common/constants.dart'; -import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/network_img_layer.dart'; -import 'package:PiliPlus/utils/utils.dart'; - -import '../../../utils/grid.dart'; - -Widget searchBangumiPanel( - context, ctr, LoadingState?> loadingState) { - late TextStyle style = TextStyle(fontSize: 13); - return switch (loadingState) { - Success() => loadingState.response?.isNotEmpty == true - ? CustomScrollView( - controller: ctr.scrollController, - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - SliverPadding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom + 80, - ), - sliver: SliverGrid( - gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: Grid.smallCardWidth * 2, - mainAxisExtent: 160, - ), - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - if (index == loadingState.response!.length - 1) { - ctr.onLoadMore(); - } - var i = loadingState.response![index]; - return InkWell( - onTap: () { - PageUtils.viewBangumi(seasonId: i.seasonId); - }, - onLongPress: () => imageSaveDialog( - context: context, - title: (i.title as List?) - ?.map((item) => item['text']) - .join() ?? - '', - cover: i.cover, - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: StyleString.safeSpace, - vertical: StyleString.cardSpace, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Stack( - children: [ - NetworkImgLayer( - width: 111, - height: 148, - src: i.cover, - ), - PBadge( - text: i.seasonTypeName, - top: 6.0, - right: 4.0, - bottom: null, - left: null, - ) - ], - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 4), - Text.rich( - // maxLines: 1, - // overflow: TextOverflow.ellipsis, - TextSpan( - style: TextStyle( - color: Theme.of(context) - .colorScheme - .onSurface), - children: [ - for (var i in i.title) ...[ - TextSpan( - text: i['text'], - style: TextStyle( - fontSize: Theme.of(context) - .textTheme - .titleSmall! - .fontSize!, - fontWeight: FontWeight.bold, - color: i['type'] == 'em' - ? Theme.of(context) - .colorScheme - .primary - : Theme.of(context) - .colorScheme - .onSurface, - ), - ), - ], - ], - ), - ), - const SizedBox(height: 12), - Text( - '评分:${i.mediaScore['score'].toString()}', - style: style), - Row( - children: [ - Text(i.areas, style: style), - const SizedBox(width: 3), - const Text('·'), - const SizedBox(width: 3), - Text( - Utils.dateFormat(i.pubtime) - .toString(), - style: style), - ], - ), - Row( - children: [ - Text(i.styles, style: style), - const SizedBox(width: 3), - const Text('·'), - const SizedBox(width: 3), - Text(i.indexShow, style: style), - ], - ), - ], - ), - ), - ], - ), - ), - ); - }, - childCount: loadingState.response!.length, - ), - ), - ), - ], - ) - : scrollErrorWidget( - callback: ctr.onReload, - ), - Error() => scrollErrorWidget( - errMsg: loadingState.errMsg, - callback: ctr.onReload, - ), - LoadingState() => throw UnimplementedError(), - }; -} diff --git a/lib/pages/search_panel/widgets/user_panel.dart b/lib/pages/search_panel/widgets/user_panel.dart deleted file mode 100644 index cce6cfcad..000000000 --- a/lib/pages/search_panel/widgets/user_panel.dart +++ /dev/null @@ -1,312 +0,0 @@ -import 'dart:math'; - -import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; -import 'package:PiliPlus/common/widgets/http_error.dart'; -import 'package:PiliPlus/http/loading_state.dart'; -import 'package:PiliPlus/pages/search/widgets/search_text.dart'; -import 'package:PiliPlus/pages/search_panel/controller.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:get/get.dart'; -import 'package:PiliPlus/common/widgets/network_img_layer.dart'; -import 'package:PiliPlus/utils/utils.dart'; - -import '../../../utils/grid.dart'; - -Widget searchUserPanel( - BuildContext context, - SearchPanelController searchPanelCtr, - LoadingState?> loadingState) { - TextStyle style = TextStyle( - fontSize: Theme.of(context).textTheme.labelSmall!.fontSize, - color: Theme.of(context).colorScheme.outline); - final ctr = Get.put(UserPanelController(), tag: searchPanelCtr.tag); - - return CustomScrollView( - controller: searchPanelCtr.scrollController, - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - SliverPersistentHeader( - pinned: false, - floating: true, - delegate: CustomSliverPersistentHeaderDelegate( - extent: 40, - bgColor: Theme.of(context).colorScheme.surface, - child: Container( - height: 40, - padding: const EdgeInsets.only(left: 25, right: 12), - child: Row( - children: [ - Obx( - () => Text( - '排序: ${ctr.orderFiltersList[ctr.currentOrderFilterval.value]['label']}', - maxLines: 1, - style: - TextStyle(color: Theme.of(context).colorScheme.outline), - ), - ), - const Spacer(), - Obx( - () => Text( - '用户类型: ${ctr.userTypeFiltersList[ctr.currentUserTypeFilterval.value]['label']}', - maxLines: 1, - style: - TextStyle(color: Theme.of(context).colorScheme.outline), - ), - ), - const Spacer(), - SizedBox( - width: 32, - height: 32, - child: IconButton( - tooltip: '筛选', - style: ButtonStyle( - padding: WidgetStateProperty.all(EdgeInsets.zero), - ), - onPressed: () { - ctr.onShowFilterDialog(context, searchPanelCtr); - }, - icon: Icon( - Icons.filter_list_outlined, - size: 18, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - ], - ), - ), - ), - ), - switch (loadingState) { - Success() => loadingState.response?.isNotEmpty == true - ? SliverPadding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom + 80, - ), - sliver: SliverGrid( - gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: Grid.smallCardWidth * 2, - mainAxisExtent: 66, - ), - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - if (index == loadingState.response!.length - 1) { - searchPanelCtr.onLoadMore(); - } - var i = loadingState.response![index]; - String heroTag = Utils.makeHeroTag(i!.mid); - return InkWell( - onTap: () => Get.toNamed('/member?mid=${i.mid}', - arguments: {'heroTag': heroTag, 'face': i.upic}), - child: Row( - children: [ - const SizedBox(width: 15), - Stack( - clipBehavior: Clip.none, - children: [ - NetworkImgLayer( - width: 42, - height: 42, - src: i.upic, - type: 'avatar', - ), - if (i.officialVerify?['type'] == 0 || - i.officialVerify?['type'] == 1) - Positioned( - bottom: 0, - right: 0, - child: Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context) - .colorScheme - .surface, - ), - child: Icon( - Icons.offline_bolt, - color: i.officialVerify?['type'] == 0 - ? Colors.yellow - : Colors.lightBlueAccent, - size: 14, - ), - ), - ), - ], - ), - const SizedBox(width: 10), - Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - children: [ - Text( - i!.uname, - style: const TextStyle( - fontSize: 14, - ), - ), - const SizedBox(width: 6), - Image.asset( - 'assets/images/lv/lv${i.isSeniorMember == 1 ? '6_s' : i!.level}.png', - height: 11, - semanticLabel: '等级${i.level}', - ), - ], - ), - Text( - '粉丝:${Utils.numFormat(i.fans)} 视频:${Utils.numFormat(i.videos)}', - style: style, - ), - if (i.officialVerify['desc'] != '') - Text( - i.officialVerify['desc'], - style: style, - ), - ], - ) - ], - ), - ); - }, - childCount: loadingState.response!.length, - ), - ), - ) - : HttpError( - callback: searchPanelCtr.onReload, - ), - Error() => HttpError( - errMsg: loadingState.errMsg, - callback: searchPanelCtr.onReload, - ), - LoadingState() => throw UnimplementedError(), - }, - ], - ); -} - -class UserPanelController extends GetxController { - List orderFiltersList = [ - {'label': '默认排序', 'value': 0, 'orderSort': 0, 'order': ''}, - {'label': '粉丝数由高到低', 'value': 1, 'orderSort': 0, 'order': 'fans'}, - {'label': '粉丝数由低到高', 'value': 2, 'orderSort': 1, 'order': 'fans'}, - {'label': 'Lv等级由高到低', 'value': 3, 'orderSort': 0, 'order': 'level'}, - {'label': 'Lv等级由低到高', 'value': 4, 'orderSort': 1, 'order': 'level'}, - ]; - List userTypeFiltersList = [ - {'label': '全部用户', 'value': 0, 'userType': 0}, - {'label': 'UP主', 'value': 1, 'userType': 1}, - {'label': '普通用户', 'value': 2, 'userType': 2}, - {'label': '认证用户', 'value': 3, 'userType': 3}, - ]; - RxInt currentOrderFilterval = 0.obs; - RxInt currentUserTypeFilterval = 0.obs; - - onShowFilterDialog( - BuildContext context, - SearchPanelController searchPanelCtr, - ) { - showModalBottomSheet( - context: context, - useSafeArea: true, - isScrollControlled: true, - clipBehavior: Clip.hardEdge, - backgroundColor: Theme.of(context).colorScheme.surface, - constraints: BoxConstraints( - maxWidth: min(640, min(Get.width, Get.height)), - ), - builder: (context) => SingleChildScrollView( - child: Container( - width: double.infinity, - padding: EdgeInsets.only( - top: 20, - left: 16, - right: 16, - bottom: 80 + MediaQuery.of(context).padding.bottom, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 10), - const Text('用户粉丝数及等级排序顺序', style: TextStyle(fontSize: 16)), - const SizedBox(height: 10), - Wrap( - spacing: 8, - runSpacing: 8, - children: orderFiltersList - .map( - (item) => SearchText( - text: item['label'], - onTap: (_) async { - Get.back(); - currentOrderFilterval.value = item['value']; - SmartDialog.dismiss(); - SmartDialog.showToast("「${item['label']}」的筛选结果"); - SearchPanelController ctr = - Get.find( - tag: searchPanelCtr.searchType.name + - searchPanelCtr.tag, - ); - ctr.orderSort = item['orderSort']; - ctr.order.value = item['order']; - SmartDialog.showLoading(msg: 'loading'); - await ctr.onReload(); - SmartDialog.dismiss(); - }, - bgColor: item['value'] == currentOrderFilterval.value - ? Theme.of(context).colorScheme.secondaryContainer - : null, - textColor: item['value'] == currentOrderFilterval.value - ? Theme.of(context).colorScheme.onSecondaryContainer - : null, - ), - ) - .toList(), - ), - const SizedBox(height: 20), - const Text('用户分类', style: TextStyle(fontSize: 16)), - const SizedBox(height: 10), - Wrap( - spacing: 8, - runSpacing: 8, - children: userTypeFiltersList - .map( - (item) => SearchText( - text: item['label'], - onTap: (_) async { - Get.back(); - currentUserTypeFilterval.value = item['value']; - SmartDialog.dismiss(); - SmartDialog.showToast("「${item['label']}」的筛选结果"); - SearchPanelController ctr = - Get.find( - tag: searchPanelCtr.searchType.name + - searchPanelCtr.tag, - ); - ctr.userType = item['userType']; - SmartDialog.showLoading(msg: 'loading'); - await ctr.onReload(); - SmartDialog.dismiss(); - }, - bgColor: item['value'] == currentUserTypeFilterval.value - ? Theme.of(context).colorScheme.secondaryContainer - : null, - textColor: item['value'] == - currentUserTypeFilterval.value - ? Theme.of(context).colorScheme.onSecondaryContainer - : null, - ), - ) - .toList(), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/pages/search_result/view.dart b/lib/pages/search_result/view.dart index baf526fd5..0d7ec9eb9 100644 --- a/lib/pages/search_result/view.dart +++ b/lib/pages/search_result/view.dart @@ -1,9 +1,14 @@ import 'package:PiliPlus/pages/search/controller.dart'; +import 'package:PiliPlus/pages/search_panel/article/view.dart'; +import 'package:PiliPlus/pages/search_panel/controller.dart'; +import 'package:PiliPlus/pages/search_panel/live/view.dart'; +import 'package:PiliPlus/pages/search_panel/pgc/view.dart'; +import 'package:PiliPlus/pages/search_panel/user/view.dart'; +import 'package:PiliPlus/pages/search_panel/video/view.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:PiliPlus/models/common/search_type.dart'; -import 'package:PiliPlus/pages/search_panel/index.dart'; import 'controller.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; @@ -86,49 +91,54 @@ class _SearchResultPageState extends State ), body: Column( children: [ - SizedBox( - width: double.infinity, - child: TabBar( - overlayColor: WidgetStateProperty.all(Colors.transparent), - splashFactory: NoSplash.splashFactory, - padding: const EdgeInsets.only(top: 4, left: 8, right: 8), - controller: _tabController, - tabs: SearchType.values - .map( - (item) => Obx( - () { - int count = _searchResultController.count[item.index]; - return Tab( - text: - '${item.label}${count != -1 ? ' ${count > 99 ? '99+' : count}' : ''}'); - }, - ), - ) - .toList(), - isScrollable: true, - indicatorWeight: 0, - indicatorPadding: - const EdgeInsets.symmetric(horizontal: 3, vertical: 8), - indicator: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - borderRadius: const BorderRadius.all(Radius.circular(20)), + SafeArea( + top: false, + bottom: false, + child: SizedBox( + width: double.infinity, + child: TabBar( + overlayColor: WidgetStateProperty.all(Colors.transparent), + splashFactory: NoSplash.splashFactory, + padding: const EdgeInsets.only(top: 4, left: 8, right: 8), + controller: _tabController, + tabs: SearchType.values + .map( + (item) => Obx( + () { + int count = _searchResultController.count[item.index]; + return Tab( + text: + '${item.label}${count != -1 ? ' ${count > 99 ? '99+' : count}' : ''}'); + }, + ), + ) + .toList(), + isScrollable: true, + indicatorWeight: 0, + indicatorPadding: + const EdgeInsets.symmetric(horizontal: 3, vertical: 8), + indicator: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + borderRadius: const BorderRadius.all(Radius.circular(20)), + ), + indicatorSize: TabBarIndicatorSize.tab, + labelColor: Theme.of(context).colorScheme.onSecondaryContainer, + labelStyle: TabBarTheme.of(context) + .labelStyle + ?.copyWith(fontSize: 13) ?? + const TextStyle(fontSize: 13), + dividerColor: Colors.transparent, + dividerHeight: 0, + unselectedLabelColor: Theme.of(context).colorScheme.outline, + tabAlignment: TabAlignment.start, + onTap: (index) { + if (_tabController.indexIsChanging.not) { + Get.find( + tag: SearchType.values[index].name + _tag) + .animateToTop(); + } + }, ), - indicatorSize: TabBarIndicatorSize.tab, - labelColor: Theme.of(context).colorScheme.onSecondaryContainer, - labelStyle: - TabBarTheme.of(context).labelStyle?.copyWith(fontSize: 13) ?? - const TextStyle(fontSize: 13), - dividerColor: Colors.transparent, - dividerHeight: 0, - unselectedLabelColor: Theme.of(context).colorScheme.outline, - tabAlignment: TabAlignment.start, - onTap: (index) { - if (_tabController.indexIsChanging.not) { - Get.find( - tag: SearchType.values[index].name + _tag) - .animateToTop(); - } - }, ), ), Expanded( @@ -138,11 +148,39 @@ class _SearchResultPageState extends State controller: _tabController, children: SearchType.values .map( - (item) => SearchPanel( - keyword: _searchResultController.keyword, - searchType: item, - tag: _tag, - ), + (item) => switch (item) { + // SearchType.all => SearchVideoPanel( + // keyword: _searchResultController.keyword, + // tag: _tag, + // ), + SearchType.video => SearchVideoPanel( + tag: _tag, + searchType: item, + keyword: _searchResultController.keyword, + ), + SearchType.media_bangumi || + SearchType.media_ft => + SearchPgcPanel( + tag: _tag, + searchType: item, + keyword: _searchResultController.keyword, + ), + SearchType.live_room => SearchLivePanel( + tag: _tag, + searchType: item, + keyword: _searchResultController.keyword, + ), + SearchType.bili_user => SearchUserPanel( + tag: _tag, + searchType: item, + keyword: _searchResultController.keyword, + ), + SearchType.article => SearchArticlePanel( + tag: _tag, + searchType: item, + keyword: _searchResultController.keyword, + ), + }, ) .toList(), ),