mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
refa: member search
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -383,6 +383,53 @@ class MemberHttp {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<LoadingState> memberArchiveNew({
|
||||
required mid,
|
||||
int ps = 25,
|
||||
int tid = 0,
|
||||
int? pn,
|
||||
String? keyword,
|
||||
String order = 'pubdate',
|
||||
bool orderAvoided = true,
|
||||
}) async {
|
||||
String dmImgStr = Utils.base64EncodeRandomString(16, 64);
|
||||
String dmCoverImgStr = Utils.base64EncodeRandomString(32, 128);
|
||||
Map params = await WbiSign.makSign({
|
||||
'mid': mid,
|
||||
'ps': ps,
|
||||
'tid': tid,
|
||||
'pn': pn,
|
||||
if (keyword != null) 'keyword': keyword,
|
||||
'order': order,
|
||||
'platform': 'web',
|
||||
'web_location': '333.1387',
|
||||
'order_avoided': orderAvoided,
|
||||
'dm_img_list': '[]',
|
||||
'dm_img_str': dmImgStr,
|
||||
'dm_cover_img_str': dmCoverImgStr,
|
||||
'dm_img_inter': '{"ds":[],"wh":[0,0,0],"of":[0,0,0]}',
|
||||
});
|
||||
var res = await Request().get(
|
||||
Api.memberArchive,
|
||||
queryParameters: params,
|
||||
options: Options(headers: {
|
||||
HttpHeaders.userAgentHeader: Request.headerUa(type: 'pc'),
|
||||
HttpHeaders.refererHeader: '${HttpString.spaceBaseUrl}/$mid',
|
||||
'origin': HttpString.spaceBaseUrl,
|
||||
}),
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return LoadingState.success(
|
||||
MemberArchiveDataModel.fromJson(res.data['data']));
|
||||
} else {
|
||||
Map errMap = {
|
||||
-352: '风控校验失败,请检查登录状态',
|
||||
};
|
||||
return LoadingState.error(
|
||||
errMap[res.data['code']] ?? res.data['message']);
|
||||
}
|
||||
}
|
||||
|
||||
// 用户动态
|
||||
static Future<LoadingState<DynamicsDataModel>> memberDynamic({
|
||||
String? offset,
|
||||
@@ -455,6 +502,30 @@ class MemberHttp {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<LoadingState> memberDynamicSearchNew({
|
||||
required int pn,
|
||||
required dynamic mid,
|
||||
required dynamic offset,
|
||||
required String keyword,
|
||||
}) async {
|
||||
var res = await Request().get(
|
||||
Api.memberDynamicSearch,
|
||||
queryParameters: {
|
||||
'host_mid': mid,
|
||||
'page': pn,
|
||||
'offset': offset,
|
||||
'keyword': keyword,
|
||||
'features': 'itemOpusStyle,listOnlyfans',
|
||||
'web_location': 333.1387,
|
||||
},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return LoadingState.success(DynamicsDataModel.fromJson(res.data['data']));
|
||||
} else {
|
||||
return LoadingState.error(res.data['message']);
|
||||
}
|
||||
}
|
||||
|
||||
// 查询分组
|
||||
static Future followUpTags() async {
|
||||
var res = await Request().get(Api.followUpTag);
|
||||
|
||||
1
lib/models/common/member/search_type.dart
Normal file
1
lib/models/common/member/search_type.dart
Normal file
@@ -0,0 +1 @@
|
||||
enum MemberSearchType { archive, dynamic }
|
||||
68
lib/pages/member_search/child/controller.dart
Normal file
68
lib/pages/member_search/child/controller.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/http/member.dart';
|
||||
import 'package:PiliPlus/models/common/member/search_type.dart';
|
||||
import 'package:PiliPlus/pages/common/common_list_controller.dart';
|
||||
import 'package:PiliPlus/pages/member_search/controller.dart';
|
||||
|
||||
class MemberSearchChildController extends CommonListController {
|
||||
MemberSearchChildController(this.controller, this.searchType);
|
||||
|
||||
final MemberSearchController controller;
|
||||
final MemberSearchType searchType;
|
||||
|
||||
dynamic offset;
|
||||
|
||||
@override
|
||||
void checkIsEnd(int length) {
|
||||
switch (searchType) {
|
||||
case MemberSearchType.archive:
|
||||
if (controller.counts.first != -1 &&
|
||||
length >= controller.counts.first) {
|
||||
isEnd = true;
|
||||
}
|
||||
break;
|
||||
case MemberSearchType.dynamic:
|
||||
if (controller.counts[1] != -1 && length >= controller.counts[1]) {
|
||||
isEnd = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
List? getDataList(response) {
|
||||
switch (searchType) {
|
||||
case MemberSearchType.archive:
|
||||
controller.counts[searchType.index] = response.page?['count'] ?? 0;
|
||||
return response.list?.vlist;
|
||||
case MemberSearchType.dynamic:
|
||||
offset = response.offset;
|
||||
controller.counts[searchType.index] = response.total ?? 0;
|
||||
return response.items;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onRefresh() {
|
||||
offset = null;
|
||||
return super.onRefresh();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LoadingState> customGetData() {
|
||||
return switch (searchType) {
|
||||
MemberSearchType.archive => MemberHttp.memberArchiveNew(
|
||||
mid: controller.mid,
|
||||
pn: currentPage,
|
||||
keyword: controller.editingController.text,
|
||||
order: 'pubdate',
|
||||
),
|
||||
MemberSearchType.dynamic => MemberHttp.memberDynamicSearchNew(
|
||||
mid: controller.mid,
|
||||
pn: currentPage,
|
||||
offset: offset ?? '',
|
||||
keyword: controller.editingController.text,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
180
lib/pages/member_search/child/view.dart
Normal file
180
lib/pages/member_search/child/view.dart
Normal file
@@ -0,0 +1,180 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/skeleton/dynamic_card.dart';
|
||||
import 'package:PiliPlus/common/skeleton/video_card_h.dart';
|
||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
||||
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
|
||||
import 'package:PiliPlus/common/widgets/video_card/video_card_h.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/models/common/member/search_type.dart';
|
||||
import 'package:PiliPlus/pages/dynamics/widgets/dynamic_panel.dart';
|
||||
import 'package:PiliPlus/pages/member_search/child/controller.dart';
|
||||
import 'package:PiliPlus/utils/grid.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:waterfall_flow/waterfall_flow.dart';
|
||||
|
||||
class MemberSearchChildPage extends StatefulWidget {
|
||||
const MemberSearchChildPage({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.searchType,
|
||||
});
|
||||
|
||||
final MemberSearchChildController controller;
|
||||
final MemberSearchType searchType;
|
||||
|
||||
@override
|
||||
State<MemberSearchChildPage> createState() => _MemberSearchChildPageState();
|
||||
}
|
||||
|
||||
class _MemberSearchChildPageState extends State<MemberSearchChildPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
late final bool dynamicsWaterfallFlow = GStorage.setting
|
||||
.get(SettingBoxKey.dynamicsWaterfallFlow, defaultValue: true);
|
||||
MemberSearchChildController get _controller => widget.controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return refreshIndicator(
|
||||
onRefresh: _controller.onRefresh,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
top: widget.searchType == MemberSearchType.archive ? 7 : 0,
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 80,
|
||||
),
|
||||
sliver: Obx(() => _buildBody(_controller.loadingState.value)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _buildLoading {
|
||||
return switch (widget.searchType) {
|
||||
MemberSearchType.archive => SliverGrid(
|
||||
gridDelegate: Grid.videoCardHDelegate(context),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return const VideoCardHSkeleton();
|
||||
},
|
||||
childCount: 10,
|
||||
),
|
||||
),
|
||||
MemberSearchType.dynamic => dynSkeleton(),
|
||||
};
|
||||
}
|
||||
|
||||
Widget dynSkeleton() {
|
||||
if (!dynamicsWaterfallFlow) {
|
||||
return SliverCrossAxisGroup(
|
||||
slivers: [
|
||||
const SliverFillRemaining(),
|
||||
SliverConstrainedCrossAxis(
|
||||
maxExtent: Grid.smallCardWidth * 2,
|
||||
sliver: SliverList.builder(
|
||||
itemBuilder: (context, index) {
|
||||
return const DynamicCardSkeleton();
|
||||
},
|
||||
itemCount: 10,
|
||||
),
|
||||
),
|
||||
const SliverFillRemaining()
|
||||
],
|
||||
);
|
||||
}
|
||||
return SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithExtentAndRatio(
|
||||
crossAxisSpacing: StyleString.cardSpace / 2,
|
||||
mainAxisSpacing: StyleString.cardSpace / 2,
|
||||
maxCrossAxisExtent: Grid.smallCardWidth * 2,
|
||||
childAspectRatio: StyleString.aspectRatio,
|
||||
mainAxisExtent: 50,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return const DynamicCardSkeleton();
|
||||
},
|
||||
childCount: 10,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(LoadingState<List?> loadingState) {
|
||||
return switch (loadingState) {
|
||||
Loading() => _buildLoading,
|
||||
Success() => loadingState.response?.isNotEmpty == true
|
||||
? Builder(
|
||||
builder: (context) {
|
||||
return switch (widget.searchType) {
|
||||
MemberSearchType.archive => SliverGrid(
|
||||
gridDelegate: Grid.videoCardHDelegate(context),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (index == loadingState.response!.length - 1) {
|
||||
_controller.onLoadMore();
|
||||
}
|
||||
return VideoCardH(
|
||||
videoItem: loadingState.response![index],
|
||||
);
|
||||
},
|
||||
childCount: loadingState.response!.length,
|
||||
),
|
||||
),
|
||||
MemberSearchType.dynamic => dynamicsWaterfallFlow
|
||||
? SliverWaterfallFlow.extent(
|
||||
maxCrossAxisExtent: Grid.smallCardWidth * 2,
|
||||
crossAxisSpacing: StyleString.safeSpace,
|
||||
mainAxisSpacing: StyleString.safeSpace,
|
||||
lastChildLayoutTypeBuilder: (index) {
|
||||
if (index == loadingState.response!.length - 1) {
|
||||
_controller.onLoadMore();
|
||||
}
|
||||
return index == loadingState.response!.length
|
||||
? LastChildLayoutType.foot
|
||||
: LastChildLayoutType.none;
|
||||
},
|
||||
children: loadingState.response!
|
||||
.map((item) => DynamicPanel(item: item))
|
||||
.toList(),
|
||||
)
|
||||
: SliverCrossAxisGroup(
|
||||
slivers: [
|
||||
const SliverFillRemaining(),
|
||||
SliverConstrainedCrossAxis(
|
||||
maxExtent: Grid.smallCardWidth * 2,
|
||||
sliver: SliverList.builder(
|
||||
itemBuilder: (context, index) {
|
||||
if (index ==
|
||||
loadingState.response!.length - 1) {
|
||||
_controller.onLoadMore();
|
||||
}
|
||||
return DynamicPanel(
|
||||
item: loadingState.response![index],
|
||||
);
|
||||
},
|
||||
itemCount: loadingState.response!.length,
|
||||
),
|
||||
),
|
||||
const SliverFillRemaining(),
|
||||
],
|
||||
),
|
||||
};
|
||||
},
|
||||
)
|
||||
: HttpError(
|
||||
onReload: _controller.onReload,
|
||||
),
|
||||
Error() => HttpError(
|
||||
errMsg: loadingState.errMsg,
|
||||
onReload: _controller.onReload,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
@@ -1,169 +1,51 @@
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/http/member.dart';
|
||||
import 'package:PiliPlus/models/dynamics/result.dart';
|
||||
import 'package:PiliPlus/models/member/archive.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/models/common/member/search_type.dart';
|
||||
import 'package:PiliPlus/pages/member_search/child/controller.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class MemberSearchController extends GetxController
|
||||
with GetSingleTickerProviderStateMixin {
|
||||
final scrollController = ScrollController();
|
||||
late final tabController = TabController(vsync: this, length: 2);
|
||||
final textEditingController = TextEditingController();
|
||||
final searchFocusNode = FocusNode();
|
||||
final editingController = TextEditingController();
|
||||
final focusNode = FocusNode();
|
||||
|
||||
RxBool hasData = false.obs;
|
||||
final mid = Get.parameters['mid'];
|
||||
final uname = Get.parameters['uname'];
|
||||
|
||||
late int mid;
|
||||
RxString uname = ''.obs;
|
||||
final RxBool hasData = false.obs;
|
||||
final RxList<int> counts = <int>[-1, -1].obs;
|
||||
|
||||
int archivePn = 1;
|
||||
RxInt archiveCount = (-1).obs;
|
||||
bool isEndArchive = false;
|
||||
Rx<LoadingState<List<VListItemModel>?>> archiveState =
|
||||
LoadingState<List<VListItemModel>?>.loading().obs;
|
||||
late final arcCtr = Get.put(
|
||||
MemberSearchChildController(this, MemberSearchType.archive),
|
||||
tag: Utils.generateRandomString(8));
|
||||
late final dynCtr = Get.put(
|
||||
MemberSearchChildController(this, MemberSearchType.dynamic),
|
||||
tag: Utils.generateRandomString(8));
|
||||
|
||||
String offset = '';
|
||||
int dynamicPn = 1;
|
||||
RxInt dynamicCount = (-1).obs;
|
||||
bool isEndDynamic = false;
|
||||
Rx<LoadingState<List<DynamicItemModel>?>> dynamicState =
|
||||
LoadingState<List<DynamicItemModel>?>.loading().obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
mid = int.parse(Get.parameters['mid']!);
|
||||
uname.value = Get.parameters['uname']!;
|
||||
}
|
||||
|
||||
// 清空搜索
|
||||
void onClear() {
|
||||
if (textEditingController.value.text.isNotEmpty) {
|
||||
textEditingController.clear();
|
||||
if (editingController.value.text.isNotEmpty) {
|
||||
editingController.clear();
|
||||
counts.value = <int>[-1, -1];
|
||||
hasData.value = false;
|
||||
searchFocusNode.requestFocus();
|
||||
focusNode.requestFocus();
|
||||
} else {
|
||||
Get.back();
|
||||
}
|
||||
}
|
||||
|
||||
// 提交搜索内容
|
||||
void submit() {
|
||||
if (textEditingController.text.isNotEmpty) {
|
||||
if (editingController.text.isNotEmpty) {
|
||||
hasData.value = true;
|
||||
|
||||
dynamicCount.value = -1;
|
||||
dynamicState.value = LoadingState.loading();
|
||||
refreshArchive();
|
||||
|
||||
archiveCount.value = -1;
|
||||
archiveState.value = LoadingState.loading();
|
||||
refreshDynamic();
|
||||
}
|
||||
}
|
||||
|
||||
Future refreshDynamic() async {
|
||||
offset = '';
|
||||
dynamicPn = 1;
|
||||
isEndDynamic = false;
|
||||
await searchDynamic();
|
||||
}
|
||||
|
||||
Future refreshArchive() async {
|
||||
archivePn = 1;
|
||||
isEndArchive = false;
|
||||
await searchArchives();
|
||||
}
|
||||
|
||||
Future searchDynamic([bool isRefresh = true]) async {
|
||||
if (isRefresh.not && isEndDynamic) return;
|
||||
dynamic res = await MemberHttp.memberDynamicSearch(
|
||||
mid: mid,
|
||||
pn: dynamicPn,
|
||||
offset: offset,
|
||||
keyword: textEditingController.text,
|
||||
);
|
||||
if (res['status']) {
|
||||
DynamicsDataModel data = res['data'];
|
||||
List<DynamicItemModel>? items = data.items;
|
||||
dynamicCount.value = data.total ?? 0;
|
||||
offset = data.offset ?? '';
|
||||
|
||||
if (data.hasMore == false || items.isNullOrEmpty) {
|
||||
isEndDynamic = true;
|
||||
if (isRefresh) {
|
||||
dynamicState.value = LoadingState.success(items);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRefresh) {
|
||||
if (items!.length >= dynamicCount.value) {
|
||||
isEndDynamic = true;
|
||||
}
|
||||
dynamicState.value = LoadingState.success(items);
|
||||
} else if (dynamicState.value is Success) {
|
||||
List<DynamicItemModel> currentList = dynamicState.value.data!
|
||||
..addAll(items!);
|
||||
if (currentList.length >= dynamicCount.value) {
|
||||
isEndDynamic = true;
|
||||
}
|
||||
dynamicState.refresh();
|
||||
}
|
||||
dynamicPn++;
|
||||
} else if (isRefresh) {
|
||||
dynamicState.value = LoadingState.error(res['msg']);
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索视频
|
||||
Future searchArchives([bool isRefresh = true]) async {
|
||||
if (isRefresh.not && isEndArchive) return;
|
||||
dynamic res = await MemberHttp.memberArchive(
|
||||
mid: mid,
|
||||
pn: archivePn,
|
||||
keyword: textEditingController.text,
|
||||
order: 'pubdate',
|
||||
);
|
||||
if (res['status']) {
|
||||
MemberArchiveDataModel data = res['data'];
|
||||
List<VListItemModel>? vlist = data.list?.vlist;
|
||||
archiveCount.value = data.page?['count'] ?? 0;
|
||||
|
||||
if (vlist.isNullOrEmpty) {
|
||||
isEndArchive = true;
|
||||
if (isRefresh) {
|
||||
archiveState.value = LoadingState.success(vlist);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRefresh) {
|
||||
if (vlist!.length >= archiveCount.value) {
|
||||
isEndArchive = true;
|
||||
}
|
||||
archiveState.value = LoadingState.success(vlist);
|
||||
} else if (archiveState.value is Success) {
|
||||
List<VListItemModel> currentList = archiveState.value.data!
|
||||
..addAll(vlist!);
|
||||
if (currentList.length >= archiveCount.value) {
|
||||
isEndArchive = true;
|
||||
}
|
||||
archiveState.refresh();
|
||||
}
|
||||
archivePn++;
|
||||
} else if (isRefresh) {
|
||||
archiveState.value = LoadingState.error(res['msg']);
|
||||
arcCtr.onReload();
|
||||
dynCtr.onReload();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
textEditingController.dispose();
|
||||
searchFocusNode.dispose();
|
||||
scrollController.dispose();
|
||||
editingController.dispose();
|
||||
focusNode.dispose();
|
||||
tabController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/skeleton/video_card_h.dart';
|
||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
||||
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
|
||||
import 'package:PiliPlus/common/widgets/video_card/video_card_h.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/models/member/archive.dart';
|
||||
import 'package:PiliPlus/pages/member_search/controller.dart';
|
||||
import 'package:PiliPlus/utils/grid.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
|
||||
|
||||
class SearchArchive extends StatefulWidget {
|
||||
const SearchArchive({
|
||||
super.key,
|
||||
required this.ctr,
|
||||
});
|
||||
|
||||
final MemberSearchController ctr;
|
||||
|
||||
@override
|
||||
State<SearchArchive> createState() => _SearchArchiveState();
|
||||
}
|
||||
|
||||
class _SearchArchiveState extends State<SearchArchive>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return refreshIndicator(
|
||||
onRefresh: () async {
|
||||
await widget.ctr.refreshArchive();
|
||||
},
|
||||
child: CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
top: StyleString.safeSpace - 5,
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 80,
|
||||
),
|
||||
sliver:
|
||||
Obx(() => _buildBody(context, widget.ctr.archiveState.value)),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(
|
||||
BuildContext context, LoadingState<List<VListItemModel>?> loadingState) {
|
||||
return switch (loadingState) {
|
||||
Loading() => SliverGrid(
|
||||
gridDelegate: Grid.videoCardHDelegate(context),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return const VideoCardHSkeleton();
|
||||
},
|
||||
childCount: 10,
|
||||
),
|
||||
),
|
||||
Success() => loadingState.response?.isNotEmpty == true
|
||||
? SliverGrid(
|
||||
gridDelegate: Grid.videoCardHDelegate(context),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (index == loadingState.response!.length - 1) {
|
||||
EasyThrottle.throttle(
|
||||
'searchArchives', const Duration(milliseconds: 500),
|
||||
() {
|
||||
widget.ctr.searchArchives(false);
|
||||
});
|
||||
}
|
||||
return VideoCardH(
|
||||
videoItem: loadingState.response![index],
|
||||
);
|
||||
},
|
||||
childCount: loadingState.response!.length,
|
||||
),
|
||||
)
|
||||
: HttpError(
|
||||
onReload: () {
|
||||
widget.ctr.archiveState.value = LoadingState.loading();
|
||||
widget.ctr.refreshArchive();
|
||||
},
|
||||
),
|
||||
Error() => HttpError(
|
||||
errMsg: loadingState.errMsg,
|
||||
onReload: () {
|
||||
widget.ctr.archiveState.value = LoadingState.loading();
|
||||
widget.ctr.refreshArchive();
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/skeleton/dynamic_card.dart';
|
||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
||||
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/models/dynamics/result.dart';
|
||||
import 'package:PiliPlus/pages/dynamics/widgets/dynamic_panel.dart';
|
||||
import 'package:PiliPlus/pages/member_search/controller.dart';
|
||||
import 'package:PiliPlus/utils/grid.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:waterfall_flow/waterfall_flow.dart';
|
||||
|
||||
class SearchDynamic extends StatefulWidget {
|
||||
const SearchDynamic({
|
||||
super.key,
|
||||
required this.ctr,
|
||||
});
|
||||
|
||||
final MemberSearchController ctr;
|
||||
|
||||
@override
|
||||
State<SearchDynamic> createState() => _SearchDynamicState();
|
||||
}
|
||||
|
||||
class _SearchDynamicState extends State<SearchDynamic>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return refreshIndicator(
|
||||
onRefresh: () async {
|
||||
await widget.ctr.refreshDynamic();
|
||||
},
|
||||
child: CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 80,
|
||||
),
|
||||
sliver:
|
||||
Obx(() => _buildBody(context, widget.ctr.dynamicState.value)),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
late final bool dynamicsWaterfallFlow = GStorage.setting
|
||||
.get(SettingBoxKey.dynamicsWaterfallFlow, defaultValue: true);
|
||||
|
||||
Widget skeleton() {
|
||||
if (!dynamicsWaterfallFlow) {
|
||||
return SliverCrossAxisGroup(
|
||||
slivers: [
|
||||
const SliverFillRemaining(),
|
||||
SliverConstrainedCrossAxis(
|
||||
maxExtent: Grid.smallCardWidth * 2,
|
||||
sliver: SliverList.builder(
|
||||
itemBuilder: (context, index) {
|
||||
return const DynamicCardSkeleton();
|
||||
},
|
||||
itemCount: 10,
|
||||
),
|
||||
),
|
||||
const SliverFillRemaining()
|
||||
],
|
||||
);
|
||||
}
|
||||
return SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithExtentAndRatio(
|
||||
crossAxisSpacing: StyleString.cardSpace / 2,
|
||||
mainAxisSpacing: StyleString.cardSpace / 2,
|
||||
maxCrossAxisExtent: Grid.smallCardWidth * 2,
|
||||
childAspectRatio: StyleString.aspectRatio,
|
||||
mainAxisExtent: 50,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return const DynamicCardSkeleton();
|
||||
},
|
||||
childCount: 10,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(BuildContext context,
|
||||
LoadingState<List<DynamicItemModel>?> loadingState) {
|
||||
return switch (loadingState) {
|
||||
Loading() => skeleton(),
|
||||
Success() => loadingState.response?.isNotEmpty == true
|
||||
? dynamicsWaterfallFlow
|
||||
? SliverWaterfallFlow.extent(
|
||||
maxCrossAxisExtent: Grid.smallCardWidth * 2,
|
||||
crossAxisSpacing: StyleString.safeSpace,
|
||||
mainAxisSpacing: StyleString.safeSpace,
|
||||
lastChildLayoutTypeBuilder: (index) {
|
||||
if (index == loadingState.response!.length - 1) {
|
||||
EasyThrottle.throttle(
|
||||
'member_dynamics', const Duration(milliseconds: 1000),
|
||||
() {
|
||||
widget.ctr.searchDynamic(false);
|
||||
});
|
||||
}
|
||||
return index == loadingState.response!.length
|
||||
? LastChildLayoutType.foot
|
||||
: LastChildLayoutType.none;
|
||||
},
|
||||
children: loadingState.response!
|
||||
.map((item) => DynamicPanel(item: item))
|
||||
.toList(),
|
||||
)
|
||||
: SliverCrossAxisGroup(
|
||||
slivers: [
|
||||
const SliverFillRemaining(),
|
||||
SliverConstrainedCrossAxis(
|
||||
maxExtent: Grid.smallCardWidth * 2,
|
||||
sliver: SliverList.builder(
|
||||
itemBuilder: (context, index) {
|
||||
if (index == loadingState.response!.length - 1) {
|
||||
EasyThrottle.throttle('member_dynamics',
|
||||
const Duration(milliseconds: 1000), () {
|
||||
widget.ctr.searchDynamic(false);
|
||||
});
|
||||
}
|
||||
return DynamicPanel(
|
||||
item: loadingState.response![index],
|
||||
);
|
||||
},
|
||||
itemCount: loadingState.response!.length,
|
||||
),
|
||||
),
|
||||
const SliverFillRemaining(),
|
||||
],
|
||||
)
|
||||
: HttpError(
|
||||
onReload: () {
|
||||
widget.ctr.dynamicState.value = LoadingState.loading();
|
||||
widget.ctr.refreshDynamic();
|
||||
},
|
||||
),
|
||||
Error() => HttpError(
|
||||
errMsg: loadingState.errMsg,
|
||||
onReload: () {
|
||||
widget.ctr.dynamicState.value = LoadingState.loading();
|
||||
widget.ctr.refreshDynamic();
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
|
||||
import 'package:PiliPlus/models/common/member/search_type.dart';
|
||||
import 'package:PiliPlus/pages/member_search/child/view.dart';
|
||||
import 'package:PiliPlus/pages/member_search/controller.dart';
|
||||
import 'package:PiliPlus/pages/member_search/search_archive.dart';
|
||||
import 'package:PiliPlus/pages/member_search/search_dynamic.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
@@ -13,7 +14,8 @@ class MemberSearchPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MemberSearchPageState extends State<MemberSearchPage> {
|
||||
final _memberSearchCtr = Get.put(MemberSearchController());
|
||||
final _controller =
|
||||
Get.put(MemberSearchController(), tag: Utils.generateRandomString(8));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -23,15 +25,15 @@ class _MemberSearchPageState extends State<MemberSearchPage> {
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: '搜索',
|
||||
onPressed: _memberSearchCtr.submit,
|
||||
onPressed: _controller.submit,
|
||||
icon: const Icon(Icons.search, size: 22),
|
||||
),
|
||||
const SizedBox(width: 10)
|
||||
],
|
||||
title: TextField(
|
||||
autofocus: true,
|
||||
focusNode: _memberSearchCtr.searchFocusNode,
|
||||
controller: _memberSearchCtr.textEditingController,
|
||||
focusNode: _controller.focusNode,
|
||||
controller: _controller.editingController,
|
||||
textInputAction: TextInputAction.search,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
decoration: InputDecoration(
|
||||
@@ -40,63 +42,80 @@ class _MemberSearchPageState extends State<MemberSearchPage> {
|
||||
suffixIcon: IconButton(
|
||||
tooltip: '清空',
|
||||
icon: const Icon(Icons.clear, size: 22),
|
||||
onPressed: _memberSearchCtr.onClear,
|
||||
onPressed: _controller.onClear,
|
||||
),
|
||||
),
|
||||
onSubmitted: (value) => _memberSearchCtr.submit(),
|
||||
onSubmitted: (value) => _controller.submit(),
|
||||
onChanged: (value) {
|
||||
if (value.isEmpty) {
|
||||
_memberSearchCtr.hasData.value = false;
|
||||
_controller.hasData.value = false;
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
body: Obx(
|
||||
() => _memberSearchCtr.hasData.value
|
||||
? SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Obx(() {
|
||||
return Opacity(
|
||||
opacity: _controller.hasData.value ? 1 : 0,
|
||||
child: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
controller: _memberSearchCtr.tabController,
|
||||
controller: _controller.tabController,
|
||||
tabs: [
|
||||
Obx(
|
||||
() => Tab(
|
||||
text:
|
||||
'视频 ${_memberSearchCtr.archiveCount.value != -1 ? '${_memberSearchCtr.archiveCount.value}' : ''}',
|
||||
'视频 ${_controller.counts[0] != -1 ? _controller.counts[0] : ''}',
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => Tab(
|
||||
text:
|
||||
'动态 ${_memberSearchCtr.dynamicCount.value != -1 ? '${_memberSearchCtr.dynamicCount.value}' : ''}',
|
||||
'动态 ${_controller.counts[1] != -1 ? _controller.counts[1] : ''}',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: tabBarView(
|
||||
controller: _memberSearchCtr.tabController,
|
||||
controller: _controller.tabController,
|
||||
children: [
|
||||
SearchArchive(ctr: _memberSearchCtr),
|
||||
SearchDynamic(ctr: _memberSearchCtr),
|
||||
MemberSearchChildPage(
|
||||
controller: _controller.arcCtr,
|
||||
searchType: MemberSearchType.archive,
|
||||
),
|
||||
MemberSearchChildPage(
|
||||
controller: _controller.dynCtr,
|
||||
searchType: MemberSearchType.dynamic,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: FractionallySizedBox(
|
||||
heightFactor: 0.5,
|
||||
widthFactor: 1.0,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'搜索「${_memberSearchCtr.uname.value}」的动态、视频',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
Obx(
|
||||
() => _controller.hasData.value
|
||||
? const SizedBox.shrink()
|
||||
: FractionallySizedBox(
|
||||
heightFactor: 0.5,
|
||||
widthFactor: 1.0,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'搜索「${_controller.uname}」的动态、视频',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user