mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-05-31 16:18:22 +08:00
refa: search panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -259,6 +259,8 @@ class Api {
|
|||||||
// 分类搜索
|
// 分类搜索
|
||||||
static const String searchByType = '/x/web-interface/search/type';
|
static const String searchByType = '/x/web-interface/search/type';
|
||||||
|
|
||||||
|
static const String searchAll = '/x/web-interface/search/all/v2';
|
||||||
|
|
||||||
// 记录视频播放进度
|
// 记录视频播放进度
|
||||||
// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/report.md
|
// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/report.md
|
||||||
static const String heartBeat = '/x/click-interface/web/heartbeat';
|
static const String heartBeat = '/x/click-interface/web/heartbeat';
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class SearchHttp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 分类搜索
|
// 分类搜索
|
||||||
static Future<LoadingState> searchByType({
|
static Future<LoadingState<R>> searchByType<R>({
|
||||||
required SearchType searchType,
|
required SearchType searchType,
|
||||||
required String keyword,
|
required String keyword,
|
||||||
required page,
|
required page,
|
||||||
@@ -56,13 +56,11 @@ class SearchHttp {
|
|||||||
int? pubBegin,
|
int? pubBegin,
|
||||||
int? pubEnd,
|
int? pubEnd,
|
||||||
}) async {
|
}) async {
|
||||||
var reqData = {
|
var params = {
|
||||||
'search_type': searchType.name,
|
'search_type': searchType.name,
|
||||||
'keyword': keyword,
|
'keyword': keyword,
|
||||||
// 'order_sort': 0,
|
|
||||||
// 'user_type': 0,
|
|
||||||
'page': page,
|
'page': page,
|
||||||
if (order != null && order.isNotEmpty) 'order': order,
|
if (order?.isNotEmpty == true) 'order': order,
|
||||||
if (duration != null) 'duration': duration,
|
if (duration != null) 'duration': duration,
|
||||||
if (tids != null) 'tids': tids,
|
if (tids != null) 'tids': tids,
|
||||||
if (orderSort != null) 'order_sort': orderSort,
|
if (orderSort != null) 'order_sort': orderSort,
|
||||||
@@ -71,7 +69,10 @@ class SearchHttp {
|
|||||||
if (pubBegin != null) 'pubtime_begin_s': pubBegin,
|
if (pubBegin != null) 'pubtime_begin_s': pubBegin,
|
||||||
if (pubEnd != null) 'pubtime_end_s': pubEnd,
|
if (pubEnd != null) 'pubtime_end_s': pubEnd,
|
||||||
};
|
};
|
||||||
var res = await Request().get(Api.searchByType, queryParameters: reqData);
|
var res = await Request().get(
|
||||||
|
Api.searchByType,
|
||||||
|
queryParameters: params,
|
||||||
|
);
|
||||||
if (res.data is! Map) {
|
if (res.data is! Map) {
|
||||||
return LoadingState.error('没有相关数据');
|
return LoadingState.error('没有相关数据');
|
||||||
}
|
}
|
||||||
@@ -101,6 +102,8 @@ class SearchHttp {
|
|||||||
case SearchType.article:
|
case SearchType.article:
|
||||||
data = SearchArticleModel.fromJson(res.data['data']);
|
data = SearchArticleModel.fromJson(res.data['data']);
|
||||||
break;
|
break;
|
||||||
|
// case SearchType.all:
|
||||||
|
// break;
|
||||||
}
|
}
|
||||||
return LoadingState.success(data);
|
return LoadingState.success(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -108,10 +111,52 @@ class SearchHttp {
|
|||||||
return LoadingState.error(err.toString());
|
return LoadingState.error(err.toString());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return LoadingState.error(
|
return LoadingState.error(res.data['message'] ?? '没有相关数据');
|
||||||
res.data['data'] != null && res.data['data']['numPages'] == 0
|
}
|
||||||
? '没有相关数据'
|
}
|
||||||
: res.data['message']);
|
|
||||||
|
static Future<LoadingState> searchAll({
|
||||||
|
required String keyword,
|
||||||
|
required page,
|
||||||
|
String? order,
|
||||||
|
int? duration,
|
||||||
|
int? tids,
|
||||||
|
int? orderSort,
|
||||||
|
int? userType,
|
||||||
|
int? categoryId,
|
||||||
|
int? pubBegin,
|
||||||
|
int? pubEnd,
|
||||||
|
}) async {
|
||||||
|
var params = {
|
||||||
|
'keyword': keyword,
|
||||||
|
'page': page,
|
||||||
|
if (order?.isNotEmpty == true) 'order': order,
|
||||||
|
if (duration != null) 'duration': duration,
|
||||||
|
if (tids != null) 'tids': tids,
|
||||||
|
if (orderSort != null) 'order_sort': orderSort,
|
||||||
|
if (userType != null) 'user_type': userType,
|
||||||
|
if (categoryId != null) 'category_id': categoryId,
|
||||||
|
if (pubBegin != null) 'pubtime_begin_s': pubBegin,
|
||||||
|
if (pubEnd != null) 'pubtime_end_s': pubEnd,
|
||||||
|
};
|
||||||
|
var res = await Request().get(
|
||||||
|
Api.searchByType,
|
||||||
|
queryParameters: params,
|
||||||
|
);
|
||||||
|
if (res.data is! Map) {
|
||||||
|
return LoadingState.error('没有相关数据');
|
||||||
|
}
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
dynamic data;
|
||||||
|
try {
|
||||||
|
// TODO
|
||||||
|
return LoadingState.success(data);
|
||||||
|
} catch (err) {
|
||||||
|
debugPrint(err.toString());
|
||||||
|
return LoadingState.error(err.toString());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return LoadingState.error(res.data['message'] ?? '没有相关数据');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// ignore_for_file: constant_identifier_names
|
// ignore_for_file: constant_identifier_names
|
||||||
enum SearchType {
|
enum SearchType {
|
||||||
|
// all,
|
||||||
// 视频:video
|
// 视频:video
|
||||||
video,
|
video,
|
||||||
// 番剧:media_bangumi,
|
// 番剧:media_bangumi,
|
||||||
@@ -23,7 +24,15 @@ enum SearchType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension SearchTypeExtension on SearchType {
|
extension SearchTypeExtension on SearchType {
|
||||||
String get label => ['视频', '番剧', '影视', '直播间', '用户', '专栏'][index];
|
String get label => [
|
||||||
|
// '综合',
|
||||||
|
'视频',
|
||||||
|
'番剧',
|
||||||
|
'影视',
|
||||||
|
'直播间',
|
||||||
|
'用户',
|
||||||
|
'专栏',
|
||||||
|
][index];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索类型为视频、专栏及相簿时
|
// 搜索类型为视频、专栏及相簿时
|
||||||
|
|||||||
@@ -4,9 +4,21 @@ import 'package:PiliPlus/utils/utils.dart';
|
|||||||
import '../model_owner.dart';
|
import '../model_owner.dart';
|
||||||
import '../model_video.dart';
|
import '../model_video.dart';
|
||||||
|
|
||||||
class SearchVideoModel {
|
abstract class SearchNumData<T> {
|
||||||
|
SearchNumData({
|
||||||
|
this.numResults,
|
||||||
|
this.list,
|
||||||
|
});
|
||||||
|
|
||||||
int? numResults;
|
int? numResults;
|
||||||
List<SearchVideoItemModel>? list;
|
List<T>? list;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchVideoModel extends SearchNumData<SearchVideoItemModel> {
|
||||||
|
SearchVideoModel({
|
||||||
|
super.numResults,
|
||||||
|
super.list,
|
||||||
|
});
|
||||||
|
|
||||||
SearchVideoModel.fromJson(Map<String, dynamic> json) {
|
SearchVideoModel.fromJson(Map<String, dynamic> json) {
|
||||||
numResults = (json['numResults'] as num?)?.toInt();
|
numResults = (json['numResults'] as num?)?.toInt();
|
||||||
@@ -88,15 +100,12 @@ class SearchOwner extends Owner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchUserModel {
|
class SearchUserModel extends SearchNumData<SearchUserItemModel> {
|
||||||
SearchUserModel({
|
SearchUserModel({
|
||||||
this.numResults,
|
super.numResults,
|
||||||
this.list,
|
super.list,
|
||||||
});
|
});
|
||||||
|
|
||||||
int? numResults;
|
|
||||||
List<SearchUserItemModel>? list;
|
|
||||||
|
|
||||||
SearchUserModel.fromJson(Map<String, dynamic> json) {
|
SearchUserModel.fromJson(Map<String, dynamic> json) {
|
||||||
numResults = (json['numResults'] as num?)?.toInt();
|
numResults = (json['numResults'] as num?)?.toInt();
|
||||||
list = (json['result'] as List?)
|
list = (json['result'] as List?)
|
||||||
@@ -165,15 +174,12 @@ class SearchUserItemModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchLiveModel {
|
class SearchLiveModel extends SearchNumData<SearchLiveItemModel> {
|
||||||
SearchLiveModel({
|
SearchLiveModel({
|
||||||
this.numResults,
|
super.numResults,
|
||||||
this.list,
|
super.list,
|
||||||
});
|
});
|
||||||
|
|
||||||
int? numResults;
|
|
||||||
List<SearchLiveItemModel>? list;
|
|
||||||
|
|
||||||
SearchLiveModel.fromJson(Map<String, dynamic> json) {
|
SearchLiveModel.fromJson(Map<String, dynamic> json) {
|
||||||
numResults = (json['numResults'] as num?)?.toInt();
|
numResults = (json['numResults'] as num?)?.toInt();
|
||||||
list = json['result']
|
list = json['result']
|
||||||
@@ -246,15 +252,12 @@ class SearchLiveItemModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchMBangumiModel {
|
class SearchMBangumiModel extends SearchNumData<SearchMBangumiItemModel> {
|
||||||
SearchMBangumiModel({
|
SearchMBangumiModel({
|
||||||
this.numResults,
|
super.numResults,
|
||||||
this.list,
|
super.list,
|
||||||
});
|
});
|
||||||
|
|
||||||
int? numResults;
|
|
||||||
List<SearchMBangumiItemModel>? list;
|
|
||||||
|
|
||||||
SearchMBangumiModel.fromJson(Map<String, dynamic> json) {
|
SearchMBangumiModel.fromJson(Map<String, dynamic> json) {
|
||||||
numResults = (json['numResults'] as num?)?.toInt();
|
numResults = (json['numResults'] as num?)?.toInt();
|
||||||
list = (json['result'] as List?)
|
list = (json['result'] as List?)
|
||||||
@@ -348,15 +351,12 @@ class SearchMBangumiItemModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchArticleModel {
|
class SearchArticleModel extends SearchNumData<SearchArticleItemModel> {
|
||||||
SearchArticleModel({
|
SearchArticleModel({
|
||||||
this.numResults,
|
super.numResults,
|
||||||
this.list,
|
super.list,
|
||||||
});
|
});
|
||||||
|
|
||||||
int? numResults;
|
|
||||||
List<SearchArticleItemModel>? list;
|
|
||||||
|
|
||||||
SearchArticleModel.fromJson(Map<String, dynamic> json) {
|
SearchArticleModel.fromJson(Map<String, dynamic> json) {
|
||||||
numResults = (json['numResults'] as num?)?.toInt();
|
numResults = (json['numResults'] as num?)?.toInt();
|
||||||
list = (json['result'] as List?)
|
list = (json['result'] as List?)
|
||||||
|
|||||||
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/http/loading_state.dart';
|
||||||
|
import 'package:PiliPlus/models/search/result.dart';
|
||||||
import 'package:PiliPlus/pages/common/common_list_controller.dart';
|
import 'package:PiliPlus/pages/common/common_list_controller.dart';
|
||||||
import 'package:PiliPlus/pages/search_result/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:get/get.dart';
|
||||||
import 'package:PiliPlus/http/search.dart';
|
import 'package:PiliPlus/http/search.dart';
|
||||||
import 'package:PiliPlus/models/common/search_type.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({
|
SearchPanelController({
|
||||||
required this.keyword,
|
required this.keyword,
|
||||||
required this.searchType,
|
required this.searchType,
|
||||||
required this.tag,
|
required this.tag,
|
||||||
});
|
});
|
||||||
String keyword;
|
final String tag;
|
||||||
SearchType searchType;
|
final String keyword;
|
||||||
// 结果排序方式 搜索类型为视频、专栏及相簿时
|
final SearchType searchType;
|
||||||
RxString order = ''.obs;
|
|
||||||
// 视频时长筛选 仅用于搜索视频
|
// sort
|
||||||
RxInt duration = 0.obs;
|
final RxString order = ''.obs;
|
||||||
|
late final RxInt duration = 0.obs;
|
||||||
int? tids;
|
int? tids;
|
||||||
int? orderSort;
|
int? orderSort;
|
||||||
int? userType;
|
int? userType;
|
||||||
int? categoryId;
|
int? categoryId;
|
||||||
String tag;
|
|
||||||
int? pubBegin;
|
int? pubBegin;
|
||||||
int? pubEnd;
|
int? pubEnd;
|
||||||
bool? hasJump2Video;
|
|
||||||
|
|
||||||
SearchResultController? searchResultController;
|
SearchResultController? searchResultController;
|
||||||
|
|
||||||
@@ -36,82 +35,28 @@ class SearchPanelController extends CommonListController {
|
|||||||
try {
|
try {
|
||||||
searchResultController = Get.find<SearchResultController>(tag: tag);
|
searchResultController = Get.find<SearchResultController>(tag: tag);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
if (searchType == SearchType.video) {
|
|
||||||
jump2Video();
|
|
||||||
} else if (searchType == SearchType.article) {
|
|
||||||
jump2Article();
|
|
||||||
}
|
|
||||||
queryData();
|
queryData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List? getDataList(response) {
|
List<T>? getDataList(R response) {
|
||||||
return response.list;
|
return response.list;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool customHandleResponse(bool isRefresh, Success response) {
|
bool customHandleResponse(bool isRefresh, Success<R> response) {
|
||||||
searchResultController?.count[searchType.index] =
|
searchResultController?.count[searchType.index] =
|
||||||
response.response.numResults;
|
response.response.numResults ?? 0;
|
||||||
|
|
||||||
if (searchType == SearchType.video && hasJump2Video != true && isRefresh) {
|
|
||||||
hasJump2Video = true;
|
|
||||||
onPushDetail(response.response.list);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
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
|
@override
|
||||||
Future<LoadingState> customGetData() => SearchHttp.searchByType(
|
Future<LoadingState<R>> customGetData() => SearchHttp.searchByType<R>(
|
||||||
searchType: searchType,
|
searchType: searchType,
|
||||||
keyword: keyword,
|
keyword: keyword,
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
order: order.value,
|
order: order.value,
|
||||||
duration: searchType.name != 'video' ? null : duration.value,
|
duration: searchType == SearchType.video ? duration.value : null,
|
||||||
tids: tids,
|
tids: tids,
|
||||||
orderSort: orderSort,
|
orderSort: orderSort,
|
||||||
userType: userType,
|
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 '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/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/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/material.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:get/get.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 '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(
|
bool? hasJump2Video;
|
||||||
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(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class VideoPanelController extends GetxController {
|
@override
|
||||||
RxList<Map> filterList = [{}].obs;
|
void onInit() {
|
||||||
Rx<ArchiveFilterType> selectedType = ArchiveFilterType.values.first.obs;
|
super.onInit();
|
||||||
List pubTimeFiltersList = [
|
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': 0},
|
||||||
{'label': '最近一天', 'value': 1},
|
{'label': '最近一天', 'value': 1},
|
||||||
{'label': '最近一周', 'value': 2},
|
{'label': '最近一周', 'value': 2},
|
||||||
{'label': '最近半年', 'value': 3},
|
{'label': '最近半年', 'value': 3},
|
||||||
];
|
];
|
||||||
List timeFiltersList = [
|
late final List timeFiltersList = [
|
||||||
{'label': '全部时长', 'value': 0},
|
{'label': '全部时长', 'value': 0},
|
||||||
{'label': '0-10分钟', 'value': 1},
|
{'label': '0-10分钟', 'value': 1},
|
||||||
{'label': '10-30分钟', 'value': 2},
|
{'label': '10-30分钟', 'value': 2},
|
||||||
{'label': '30-60分钟', 'value': 3},
|
{'label': '30-60分钟', 'value': 3},
|
||||||
{'label': '60分钟+', 'value': 4},
|
{'label': '60分钟+', 'value': 4},
|
||||||
];
|
];
|
||||||
List zoneFiltersList = [
|
late final List zoneFiltersList = [
|
||||||
{'label': '全部', 'value': 0},
|
{'label': '全部', 'value': 0},
|
||||||
{'label': '动画', 'value': 1, 'tids': 1},
|
{'label': '动画', 'value': 1, 'tids': 1},
|
||||||
{'label': '番剧', 'value': 2, 'tids': 13},
|
{'label': '番剧', 'value': 2, 'tids': 13},
|
||||||
@@ -164,47 +132,15 @@ class VideoPanelController extends GetxController {
|
|||||||
{'label': '电影', 'value': 20, 'tids': 23},
|
{'label': '电影', 'value': 20, 'tids': 23},
|
||||||
{'label': '电视', 'value': 21, 'tids': 11},
|
{'label': '电视', 'value': 21, 'tids': 11},
|
||||||
];
|
];
|
||||||
int currentPubTimeFilterval = 0;
|
int currentPubTimeFilter = 0;
|
||||||
late DateTime pubBegin;
|
late DateTime pubBeginDate;
|
||||||
late DateTime pubEnd;
|
late DateTime pubEndDate;
|
||||||
bool customPubBegin = false;
|
bool customPubBeginDate = false;
|
||||||
bool customPubEnd = false;
|
bool customPubEndDate = false;
|
||||||
int currentTimeFilterval = 0;
|
int currentTimeFilter = 0;
|
||||||
int currentZoneFilterval = 0;
|
int currentZoneFilter = 0;
|
||||||
|
|
||||||
@override
|
onShowFilterDialog(BuildContext context) {
|
||||||
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,
|
|
||||||
) {
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
useSafeArea: true,
|
useSafeArea: true,
|
||||||
@@ -218,43 +154,39 @@ class VideoPanelController extends GetxController {
|
|||||||
builder: (context, setState) {
|
builder: (context, setState) {
|
||||||
Widget dateWidget([bool isFirst = true]) {
|
Widget dateWidget([bool isFirst = true]) {
|
||||||
return SearchText(
|
return SearchText(
|
||||||
text:
|
text: DateFormat('yyyy-MM-dd')
|
||||||
DateFormat('yyyy-MM-dd').format(isFirst ? pubBegin : pubEnd),
|
.format(isFirst ? pubBeginDate : pubEndDate),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
onTap: (text) {
|
onTap: (text) {
|
||||||
showDatePicker(
|
showDatePicker(
|
||||||
context: context,
|
context: context,
|
||||||
initialDate: isFirst ? pubBegin : pubEnd,
|
initialDate: isFirst ? pubBeginDate : pubEndDate,
|
||||||
firstDate: isFirst ? DateTime(2009, 6, 26) : pubBegin,
|
firstDate: isFirst ? DateTime(2009, 6, 26) : pubBeginDate,
|
||||||
lastDate: isFirst ? pubEnd : DateTime.now(),
|
lastDate: isFirst ? pubEndDate : DateTime.now(),
|
||||||
).then((selectedDate) async {
|
).then((selectedDate) async {
|
||||||
if (selectedDate != null) {
|
if (selectedDate != null) {
|
||||||
if (isFirst) {
|
if (isFirst) {
|
||||||
customPubBegin = true;
|
customPubBeginDate = true;
|
||||||
pubBegin = selectedDate;
|
pubBeginDate = selectedDate;
|
||||||
} else {
|
} else {
|
||||||
customPubEnd = true;
|
customPubEndDate = true;
|
||||||
pubEnd = selectedDate;
|
pubEndDate = selectedDate;
|
||||||
}
|
}
|
||||||
currentPubTimeFilterval = -1;
|
currentPubTimeFilter = -1;
|
||||||
SmartDialog.dismiss();
|
SmartDialog.dismiss();
|
||||||
// SmartDialog.showToast("「${item['label']}」的筛选结果");
|
pubBegin = DateTime(
|
||||||
SearchPanelController ctr = Get.find<SearchPanelController>(
|
pubBeginDate.year,
|
||||||
tag: searchPanelCtr.searchType.name + searchPanelCtr.tag,
|
pubBeginDate.month,
|
||||||
);
|
pubBeginDate.day,
|
||||||
ctr.pubBegin = DateTime(
|
|
||||||
pubBegin.year,
|
|
||||||
pubBegin.month,
|
|
||||||
pubBegin.day,
|
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
).millisecondsSinceEpoch ~/
|
).millisecondsSinceEpoch ~/
|
||||||
1000;
|
1000;
|
||||||
ctr.pubEnd = DateTime(
|
pubEnd = DateTime(
|
||||||
pubEnd.year,
|
pubEndDate.year,
|
||||||
pubEnd.month,
|
pubEndDate.month,
|
||||||
pubEnd.day,
|
pubEndDate.day,
|
||||||
23,
|
23,
|
||||||
59,
|
59,
|
||||||
59,
|
59,
|
||||||
@@ -262,17 +194,17 @@ class VideoPanelController extends GetxController {
|
|||||||
1000;
|
1000;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
SmartDialog.showLoading(msg: 'loading');
|
SmartDialog.showLoading(msg: 'loading');
|
||||||
await ctr.onReload();
|
await onReload();
|
||||||
SmartDialog.dismiss();
|
SmartDialog.dismiss();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
bgColor: currentPubTimeFilterval == -1 &&
|
bgColor: currentPubTimeFilter == -1 &&
|
||||||
(isFirst ? customPubBegin : customPubEnd)
|
(isFirst ? customPubBeginDate : customPubEndDate)
|
||||||
? Theme.of(context).colorScheme.secondaryContainer
|
? Theme.of(context).colorScheme.secondaryContainer
|
||||||
: Theme.of(context).colorScheme.outline.withOpacity(0.1),
|
: Theme.of(context).colorScheme.outline.withOpacity(0.1),
|
||||||
textColor: currentPubTimeFilterval == -1 &&
|
textColor: currentPubTimeFilter == -1 &&
|
||||||
(isFirst ? customPubBegin : customPubEnd)
|
(isFirst ? customPubBeginDate : customPubEndDate)
|
||||||
? Theme.of(context).colorScheme.onSecondaryContainer
|
? Theme.of(context).colorScheme.onSecondaryContainer
|
||||||
: Theme.of(context).colorScheme.outline.withOpacity(0.8),
|
: Theme.of(context).colorScheme.outline.withOpacity(0.8),
|
||||||
);
|
);
|
||||||
@@ -303,20 +235,15 @@ class VideoPanelController extends GetxController {
|
|||||||
text: item['label'],
|
text: item['label'],
|
||||||
onTap: (text) async {
|
onTap: (text) async {
|
||||||
Get.back();
|
Get.back();
|
||||||
currentPubTimeFilterval = item['value'];
|
currentPubTimeFilter = item['value'];
|
||||||
SmartDialog.dismiss();
|
SmartDialog.dismiss();
|
||||||
SmartDialog.showToast("「${item['label']}」的筛选结果");
|
SmartDialog.showToast("「${item['label']}」的筛选结果");
|
||||||
SearchPanelController ctr =
|
|
||||||
Get.find<SearchPanelController>(
|
|
||||||
tag: searchPanelCtr.searchType.name +
|
|
||||||
searchPanelCtr.tag,
|
|
||||||
);
|
|
||||||
DateTime now = DateTime.now();
|
DateTime now = DateTime.now();
|
||||||
if (item['value'] == 0) {
|
if (item['value'] == 0) {
|
||||||
ctr.pubBegin = null;
|
pubBegin = null;
|
||||||
ctr.pubEnd = null;
|
pubEnd = null;
|
||||||
} else {
|
} else {
|
||||||
ctr.pubBegin = DateTime(
|
pubBegin = DateTime(
|
||||||
now.year,
|
now.year,
|
||||||
now.month,
|
now.month,
|
||||||
now.day -
|
now.day -
|
||||||
@@ -330,7 +257,7 @@ class VideoPanelController extends GetxController {
|
|||||||
0,
|
0,
|
||||||
).millisecondsSinceEpoch ~/
|
).millisecondsSinceEpoch ~/
|
||||||
1000;
|
1000;
|
||||||
ctr.pubEnd = DateTime(
|
pubEnd = DateTime(
|
||||||
now.year,
|
now.year,
|
||||||
now.month,
|
now.month,
|
||||||
now.day,
|
now.day,
|
||||||
@@ -341,15 +268,15 @@ class VideoPanelController extends GetxController {
|
|||||||
1000;
|
1000;
|
||||||
}
|
}
|
||||||
SmartDialog.showLoading(msg: 'loading');
|
SmartDialog.showLoading(msg: 'loading');
|
||||||
await ctr.onReload();
|
await onReload();
|
||||||
SmartDialog.dismiss();
|
SmartDialog.dismiss();
|
||||||
},
|
},
|
||||||
bgColor: item['value'] == currentPubTimeFilterval
|
bgColor: item['value'] == currentPubTimeFilter
|
||||||
? Theme.of(context)
|
? Theme.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.secondaryContainer
|
.secondaryContainer
|
||||||
: null,
|
: null,
|
||||||
textColor: item['value'] == currentPubTimeFilterval
|
textColor: item['value'] == currentPubTimeFilter
|
||||||
? Theme.of(context)
|
? Theme.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.onSecondaryContainer
|
.onSecondaryContainer
|
||||||
@@ -383,25 +310,20 @@ class VideoPanelController extends GetxController {
|
|||||||
text: item['label'],
|
text: item['label'],
|
||||||
onTap: (text) async {
|
onTap: (text) async {
|
||||||
Get.back();
|
Get.back();
|
||||||
currentTimeFilterval = item['value'];
|
currentTimeFilter = item['value'];
|
||||||
SmartDialog.dismiss();
|
SmartDialog.dismiss();
|
||||||
SmartDialog.showToast("「${item['label']}」的筛选结果");
|
SmartDialog.showToast("「${item['label']}」的筛选结果");
|
||||||
SearchPanelController ctr =
|
duration.value = item['value'];
|
||||||
Get.find<SearchPanelController>(
|
|
||||||
tag: searchPanelCtr.searchType.name +
|
|
||||||
searchPanelCtr.tag,
|
|
||||||
);
|
|
||||||
ctr.duration.value = item['value'];
|
|
||||||
SmartDialog.showLoading(msg: 'loading');
|
SmartDialog.showLoading(msg: 'loading');
|
||||||
await ctr.onReload();
|
await onReload();
|
||||||
SmartDialog.dismiss();
|
SmartDialog.dismiss();
|
||||||
},
|
},
|
||||||
bgColor: item['value'] == currentTimeFilterval
|
bgColor: item['value'] == currentTimeFilter
|
||||||
? Theme.of(context)
|
? Theme.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.secondaryContainer
|
.secondaryContainer
|
||||||
: null,
|
: null,
|
||||||
textColor: item['value'] == currentTimeFilterval
|
textColor: item['value'] == currentTimeFilter
|
||||||
? Theme.of(context)
|
? Theme.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.onSecondaryContainer
|
.onSecondaryContainer
|
||||||
@@ -422,25 +344,20 @@ class VideoPanelController extends GetxController {
|
|||||||
text: item['label'],
|
text: item['label'],
|
||||||
onTap: (text) async {
|
onTap: (text) async {
|
||||||
Get.back();
|
Get.back();
|
||||||
currentZoneFilterval = item['value'];
|
currentZoneFilter = item['value'];
|
||||||
SmartDialog.dismiss();
|
SmartDialog.dismiss();
|
||||||
SmartDialog.showToast("「${item['label']}」的筛选结果");
|
SmartDialog.showToast("「${item['label']}」的筛选结果");
|
||||||
SearchPanelController ctr =
|
tids = item['tids'];
|
||||||
Get.find<SearchPanelController>(
|
|
||||||
tag: searchPanelCtr.searchType.name +
|
|
||||||
searchPanelCtr.tag,
|
|
||||||
);
|
|
||||||
ctr.tids = item['tids'];
|
|
||||||
SmartDialog.showLoading(msg: 'loading');
|
SmartDialog.showLoading(msg: 'loading');
|
||||||
await ctr.onReload();
|
await onReload();
|
||||||
SmartDialog.dismiss();
|
SmartDialog.dismiss();
|
||||||
},
|
},
|
||||||
bgColor: item['value'] == currentZoneFilterval
|
bgColor: item['value'] == currentZoneFilter
|
||||||
? Theme.of(context)
|
? Theme.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.secondaryContainer
|
.secondaryContainer
|
||||||
: null,
|
: null,
|
||||||
textColor: item['value'] == currentZoneFilterval
|
textColor: item['value'] == currentZoneFilter
|
||||||
? Theme.of(context)
|
? Theme.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.onSecondaryContainer
|
.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/common/constants.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/skeleton/media_bangumi.dart';
|
import 'package:PiliPlus/common/skeleton/media_bangumi.dart';
|
||||||
import 'package:PiliPlus/common/skeleton/video_card_h.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 'package:PiliPlus/models/common/search_type.dart';
|
||||||
|
|
||||||
import '../../common/constants.dart';
|
|
||||||
import '../../utils/grid.dart';
|
|
||||||
import 'controller.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 {
|
abstract class CommonSearchPanel extends StatefulWidget {
|
||||||
final String keyword;
|
const CommonSearchPanel({
|
||||||
final SearchType searchType;
|
|
||||||
final String tag;
|
|
||||||
const SearchPanel({
|
|
||||||
super.key,
|
super.key,
|
||||||
required this.keyword,
|
required this.keyword,
|
||||||
required this.searchType,
|
required this.searchType,
|
||||||
required this.tag,
|
required this.tag,
|
||||||
|
this.hasHeader = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
final String keyword;
|
||||||
State<SearchPanel> createState() => _SearchPanelState();
|
final SearchType searchType;
|
||||||
|
final String tag;
|
||||||
|
final bool hasHeader;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SearchPanelState extends State<SearchPanel>
|
abstract class CommonSearchPanelState<
|
||||||
with AutomaticKeepAliveClientMixin {
|
S extends CommonSearchPanel,
|
||||||
late SearchPanelController _searchPanelController;
|
R extends SearchNumData<T>,
|
||||||
|
T> extends State<S> with AutomaticKeepAliveClientMixin {
|
||||||
|
SearchPanelController<R, T> get controller;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => true;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
return refreshIndicator(
|
return refreshIndicator(
|
||||||
onRefresh: () async {
|
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) {
|
Widget buildHeader(LoadingState<List<T>?> loadingState) {
|
||||||
if (loadingState is Loading) {
|
throw UnimplementedError();
|
||||||
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 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(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
import 'package:PiliPlus/pages/search/controller.dart';
|
import 'package:PiliPlus/pages/search/controller.dart';
|
||||||
|
import 'package:PiliPlus/pages/search_panel/article/view.dart';
|
||||||
|
import 'package:PiliPlus/pages/search_panel/controller.dart';
|
||||||
|
import 'package:PiliPlus/pages/search_panel/live/view.dart';
|
||||||
|
import 'package:PiliPlus/pages/search_panel/pgc/view.dart';
|
||||||
|
import 'package:PiliPlus/pages/search_panel/user/view.dart';
|
||||||
|
import 'package:PiliPlus/pages/search_panel/video/view.dart';
|
||||||
import 'package:PiliPlus/utils/extension.dart';
|
import 'package:PiliPlus/utils/extension.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:PiliPlus/models/common/search_type.dart';
|
import 'package:PiliPlus/models/common/search_type.dart';
|
||||||
import 'package:PiliPlus/pages/search_panel/index.dart';
|
|
||||||
import 'controller.dart';
|
import 'controller.dart';
|
||||||
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
|
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
|
||||||
|
|
||||||
@@ -86,49 +91,54 @@ class _SearchResultPageState extends State<SearchResultPage>
|
|||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SafeArea(
|
||||||
width: double.infinity,
|
top: false,
|
||||||
child: TabBar(
|
bottom: false,
|
||||||
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
child: SizedBox(
|
||||||
splashFactory: NoSplash.splashFactory,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.only(top: 4, left: 8, right: 8),
|
child: TabBar(
|
||||||
controller: _tabController,
|
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
||||||
tabs: SearchType.values
|
splashFactory: NoSplash.splashFactory,
|
||||||
.map(
|
padding: const EdgeInsets.only(top: 4, left: 8, right: 8),
|
||||||
(item) => Obx(
|
controller: _tabController,
|
||||||
() {
|
tabs: SearchType.values
|
||||||
int count = _searchResultController.count[item.index];
|
.map(
|
||||||
return Tab(
|
(item) => Obx(
|
||||||
text:
|
() {
|
||||||
'${item.label}${count != -1 ? ' ${count > 99 ? '99+' : count}' : ''}');
|
int count = _searchResultController.count[item.index];
|
||||||
},
|
return Tab(
|
||||||
),
|
text:
|
||||||
)
|
'${item.label}${count != -1 ? ' ${count > 99 ? '99+' : count}' : ''}');
|
||||||
.toList(),
|
},
|
||||||
isScrollable: true,
|
),
|
||||||
indicatorWeight: 0,
|
)
|
||||||
indicatorPadding:
|
.toList(),
|
||||||
const EdgeInsets.symmetric(horizontal: 3, vertical: 8),
|
isScrollable: true,
|
||||||
indicator: BoxDecoration(
|
indicatorWeight: 0,
|
||||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
indicatorPadding:
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
const EdgeInsets.symmetric(horizontal: 3, vertical: 8),
|
||||||
|
indicator: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||||
|
),
|
||||||
|
indicatorSize: TabBarIndicatorSize.tab,
|
||||||
|
labelColor: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||||
|
labelStyle: TabBarTheme.of(context)
|
||||||
|
.labelStyle
|
||||||
|
?.copyWith(fontSize: 13) ??
|
||||||
|
const TextStyle(fontSize: 13),
|
||||||
|
dividerColor: Colors.transparent,
|
||||||
|
dividerHeight: 0,
|
||||||
|
unselectedLabelColor: Theme.of(context).colorScheme.outline,
|
||||||
|
tabAlignment: TabAlignment.start,
|
||||||
|
onTap: (index) {
|
||||||
|
if (_tabController.indexIsChanging.not) {
|
||||||
|
Get.find<SearchPanelController>(
|
||||||
|
tag: SearchType.values[index].name + _tag)
|
||||||
|
.animateToTop();
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
indicatorSize: TabBarIndicatorSize.tab,
|
|
||||||
labelColor: Theme.of(context).colorScheme.onSecondaryContainer,
|
|
||||||
labelStyle:
|
|
||||||
TabBarTheme.of(context).labelStyle?.copyWith(fontSize: 13) ??
|
|
||||||
const TextStyle(fontSize: 13),
|
|
||||||
dividerColor: Colors.transparent,
|
|
||||||
dividerHeight: 0,
|
|
||||||
unselectedLabelColor: Theme.of(context).colorScheme.outline,
|
|
||||||
tabAlignment: TabAlignment.start,
|
|
||||||
onTap: (index) {
|
|
||||||
if (_tabController.indexIsChanging.not) {
|
|
||||||
Get.find<SearchPanelController>(
|
|
||||||
tag: SearchType.values[index].name + _tag)
|
|
||||||
.animateToTop();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -138,11 +148,39 @@ class _SearchResultPageState extends State<SearchResultPage>
|
|||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
children: SearchType.values
|
children: SearchType.values
|
||||||
.map(
|
.map(
|
||||||
(item) => SearchPanel(
|
(item) => switch (item) {
|
||||||
keyword: _searchResultController.keyword,
|
// SearchType.all => SearchVideoPanel(
|
||||||
searchType: item,
|
// keyword: _searchResultController.keyword,
|
||||||
tag: _tag,
|
// tag: _tag,
|
||||||
),
|
// ),
|
||||||
|
SearchType.video => SearchVideoPanel(
|
||||||
|
tag: _tag,
|
||||||
|
searchType: item,
|
||||||
|
keyword: _searchResultController.keyword,
|
||||||
|
),
|
||||||
|
SearchType.media_bangumi ||
|
||||||
|
SearchType.media_ft =>
|
||||||
|
SearchPgcPanel(
|
||||||
|
tag: _tag,
|
||||||
|
searchType: item,
|
||||||
|
keyword: _searchResultController.keyword,
|
||||||
|
),
|
||||||
|
SearchType.live_room => SearchLivePanel(
|
||||||
|
tag: _tag,
|
||||||
|
searchType: item,
|
||||||
|
keyword: _searchResultController.keyword,
|
||||||
|
),
|
||||||
|
SearchType.bili_user => SearchUserPanel(
|
||||||
|
tag: _tag,
|
||||||
|
searchType: item,
|
||||||
|
keyword: _searchResultController.keyword,
|
||||||
|
),
|
||||||
|
SearchType.article => SearchArticlePanel(
|
||||||
|
tag: _tag,
|
||||||
|
searchType: item,
|
||||||
|
keyword: _searchResultController.keyword,
|
||||||
|
),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user