refa: search panel

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-04-17 16:13:11 +08:00
parent 7b9e4b2f82
commit 4db7711a36
21 changed files with 1607 additions and 1504 deletions

View File

@@ -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<SearchArticleModel, SearchArticleItemModel> {
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(),
),
],
),
),
),
);
}
}

View File

@@ -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<SearchArticlePanel> 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<List<SearchArticleItemModel>?> 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<SearchArticleItemModel> 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: <Widget>[
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,
),
),
);
}
}

View File

@@ -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<R extends SearchNumData<T>, T>
extends CommonListController<R, T> {
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<SearchResultController>(tag: tag);
} catch (_) {}
if (searchType == SearchType.video) {
jump2Video();
} else if (searchType == SearchType.article) {
jump2Article();
}
queryData();
}
@override
List? getDataList(response) {
List<T>? getDataList(R response) {
return response.list;
}
@override
bool customHandleResponse(bool isRefresh, Success response) {
bool customHandleResponse(bool isRefresh, Success<R> 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<LoadingState> customGetData() => SearchHttp.searchByType(
Future<LoadingState<R>> customGetData() => SearchHttp.searchByType<R>(
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,

View File

@@ -1,4 +0,0 @@
library searchpanel;
export './controller.dart';
export './view.dart';

View File

@@ -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<SearchLivePanel> createState() => _SearchLivePanelState();
}
class _SearchLivePanelState extends CommonSearchPanelState<SearchLivePanel,
SearchLiveModel, SearchLiveItemModel> {
@override
late final controller = Get.put(
SearchPanelController<SearchLiveModel, SearchLiveItemModel>(
keyword: widget.keyword,
searchType: widget.searchType,
tag: widget.tag,
),
tag: widget.searchType.name + widget.tag,
);
@override
Widget buildList(List<SearchLiveItemModel> 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,
),
),
);
}
}

View File

@@ -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: <Color>[
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),
)
],
),
);
}
}

View File

@@ -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<SearchPgcPanel> createState() => _SearchPgcPanelState();
}
class _SearchPgcPanelState extends CommonSearchPanelState<SearchPgcPanel,
SearchMBangumiModel, SearchMBangumiItemModel> {
@override
late final controller = Get.put(
SearchPanelController<SearchMBangumiModel, SearchMBangumiItemModel>(
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<SearchMBangumiItemModel> 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,
),
),
);
}
}

View File

@@ -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<SearchUserModel, SearchUserItemModel> {
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(),
),
],
),
),
),
);
}
}

View File

@@ -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<SearchUserPanel> createState() => _SearchUserPanelState();
}
class _SearchUserPanelState extends CommonSearchPanelState<SearchUserPanel,
SearchUserModel, SearchUserItemModel> {
@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<List<SearchUserItemModel>?> 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<SearchUserItemModel> 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,
),
),
);
}
}

View File

@@ -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<SearchVideoModel, SearchVideoItemModel> {
SearchVideoController({
required super.keyword,
required super.searchType,
required super.tag,
});
Widget searchVideoPanel(
BuildContext context,
SearchPanelController searchPanelCtr,
LoadingState<List<dynamic>?> 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<Map> filterList = [{}].obs;
Rx<ArchiveFilterType> 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<SearchVideoItemModel>? getDataList(SearchVideoModel response) {
return response.list;
}
@override
bool customHandleResponse(
bool isRefresh, Success<SearchVideoModel> 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<Map> filterList = ArchiveFilterType.values
.map((type) => {
'label': type.description,
'type': type,
})
.toList();
late final Rx<ArchiveFilterType> 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<Map<String, dynamic>> 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<SearchPanelController>(
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<SearchPanelController>(
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<SearchPanelController>(
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<SearchPanelController>(
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

View File

@@ -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<SearchVideoPanel> createState() => _SearchVideoPanelState();
}
class _SearchVideoPanelState extends CommonSearchPanelState<SearchVideoPanel,
SearchVideoModel, SearchVideoItemModel> {
@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<List<SearchVideoItemModel>?> 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<SearchVideoItemModel> 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,
),
),
);
}
}

View File

@@ -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<SearchPanel> createState() => _SearchPanelState();
final String keyword;
final SearchType searchType;
final String tag;
final bool hasHeader;
}
class _SearchPanelState extends State<SearchPanel>
with AutomaticKeepAliveClientMixin {
late SearchPanelController _searchPanelController;
abstract class CommonSearchPanelState<
S extends CommonSearchPanel,
R extends SearchNumData<T>,
T> extends State<S> with AutomaticKeepAliveClientMixin {
SearchPanelController<R, T> 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<List<dynamic>?> 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<List<T>?> 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<List<T>?> 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<T> list);
}

View File

@@ -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<List<dynamic>?> 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: <Widget>[
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<SearchPanelController>(
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<SearchPanelController>(
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(),
),
],
),
),
),
);
}
}

View File

@@ -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<List<dynamic>?> 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: <Color>[
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),
)
],
),
);
}
}

View File

@@ -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<List<dynamic>?> 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(),
};
}

View File

@@ -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<List<dynamic>?> 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<SearchPanelController>(
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<SearchPanelController>(
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(),
),
],
),
),
),
);
}
}