mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
refa: search panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
154
lib/pages/search_panel/article/controller.dart
Normal file
154
lib/pages/search_panel/article/controller.dart
Normal 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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
225
lib/pages/search_panel/article/view.dart
Normal file
225
lib/pages/search_panel/article/view.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
library searchpanel;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
||||
62
lib/pages/search_panel/live/view.dart
Normal file
62
lib/pages/search_panel/live/view.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
140
lib/pages/search_panel/live/widgets/item.dart
Normal file
140
lib/pages/search_panel/live/widgets/item.dart
Normal 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),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
159
lib/pages/search_panel/pgc/view.dart
Normal file
159
lib/pages/search_panel/pgc/view.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
126
lib/pages/search_panel/user/controller.dart
Normal file
126
lib/pages/search_panel/user/controller.dart
Normal 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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
202
lib/pages/search_panel/user/view.dart
Normal file
202
lib/pages/search_panel/user/view.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
130
lib/pages/search_panel/video/view.dart
Normal file
130
lib/pages/search_panel/video/view.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user