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({
|
static Future<LoadingState<DynamicsDataModel>> memberDynamic({
|
||||||
String? offset,
|
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 {
|
static Future followUpTags() async {
|
||||||
var res = await Request().get(Api.followUpTag);
|
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/models/common/member/search_type.dart';
|
||||||
import 'package:PiliPlus/http/member.dart';
|
import 'package:PiliPlus/pages/member_search/child/controller.dart';
|
||||||
import 'package:PiliPlus/models/dynamics/result.dart';
|
import 'package:PiliPlus/utils/utils.dart';
|
||||||
import 'package:PiliPlus/models/member/archive.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';
|
||||||
|
|
||||||
class MemberSearchController extends GetxController
|
class MemberSearchController extends GetxController
|
||||||
with GetSingleTickerProviderStateMixin {
|
with GetSingleTickerProviderStateMixin {
|
||||||
final scrollController = ScrollController();
|
|
||||||
late final tabController = TabController(vsync: this, length: 2);
|
late final tabController = TabController(vsync: this, length: 2);
|
||||||
final textEditingController = TextEditingController();
|
final editingController = TextEditingController();
|
||||||
final searchFocusNode = FocusNode();
|
final focusNode = FocusNode();
|
||||||
|
|
||||||
RxBool hasData = false.obs;
|
final mid = Get.parameters['mid'];
|
||||||
|
final uname = Get.parameters['uname'];
|
||||||
|
|
||||||
late int mid;
|
final RxBool hasData = false.obs;
|
||||||
RxString uname = ''.obs;
|
final RxList<int> counts = <int>[-1, -1].obs;
|
||||||
|
|
||||||
int archivePn = 1;
|
late final arcCtr = Get.put(
|
||||||
RxInt archiveCount = (-1).obs;
|
MemberSearchChildController(this, MemberSearchType.archive),
|
||||||
bool isEndArchive = false;
|
tag: Utils.generateRandomString(8));
|
||||||
Rx<LoadingState<List<VListItemModel>?>> archiveState =
|
late final dynCtr = Get.put(
|
||||||
LoadingState<List<VListItemModel>?>.loading().obs;
|
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() {
|
void onClear() {
|
||||||
if (textEditingController.value.text.isNotEmpty) {
|
if (editingController.value.text.isNotEmpty) {
|
||||||
textEditingController.clear();
|
editingController.clear();
|
||||||
|
counts.value = <int>[-1, -1];
|
||||||
hasData.value = false;
|
hasData.value = false;
|
||||||
searchFocusNode.requestFocus();
|
focusNode.requestFocus();
|
||||||
} else {
|
} else {
|
||||||
Get.back();
|
Get.back();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交搜索内容
|
|
||||||
void submit() {
|
void submit() {
|
||||||
if (textEditingController.text.isNotEmpty) {
|
if (editingController.text.isNotEmpty) {
|
||||||
hasData.value = true;
|
hasData.value = true;
|
||||||
|
arcCtr.onReload();
|
||||||
dynamicCount.value = -1;
|
dynCtr.onReload();
|
||||||
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']);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
textEditingController.dispose();
|
editingController.dispose();
|
||||||
searchFocusNode.dispose();
|
focusNode.dispose();
|
||||||
scrollController.dispose();
|
|
||||||
tabController.dispose();
|
tabController.dispose();
|
||||||
super.onClose();
|
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/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/controller.dart';
|
||||||
import 'package:PiliPlus/pages/member_search/search_archive.dart';
|
import 'package:PiliPlus/utils/utils.dart';
|
||||||
import 'package:PiliPlus/pages/member_search/search_dynamic.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
@@ -13,7 +14,8 @@ class MemberSearchPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MemberSearchPageState extends State<MemberSearchPage> {
|
class _MemberSearchPageState extends State<MemberSearchPage> {
|
||||||
final _memberSearchCtr = Get.put(MemberSearchController());
|
final _controller =
|
||||||
|
Get.put(MemberSearchController(), tag: Utils.generateRandomString(8));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -23,15 +25,15 @@ class _MemberSearchPageState extends State<MemberSearchPage> {
|
|||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: '搜索',
|
tooltip: '搜索',
|
||||||
onPressed: _memberSearchCtr.submit,
|
onPressed: _controller.submit,
|
||||||
icon: const Icon(Icons.search, size: 22),
|
icon: const Icon(Icons.search, size: 22),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10)
|
const SizedBox(width: 10)
|
||||||
],
|
],
|
||||||
title: TextField(
|
title: TextField(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
focusNode: _memberSearchCtr.searchFocusNode,
|
focusNode: _controller.focusNode,
|
||||||
controller: _memberSearchCtr.textEditingController,
|
controller: _controller.editingController,
|
||||||
textInputAction: TextInputAction.search,
|
textInputAction: TextInputAction.search,
|
||||||
textAlignVertical: TextAlignVertical.center,
|
textAlignVertical: TextAlignVertical.center,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
@@ -40,63 +42,80 @@ class _MemberSearchPageState extends State<MemberSearchPage> {
|
|||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
tooltip: '清空',
|
tooltip: '清空',
|
||||||
icon: const Icon(Icons.clear, size: 22),
|
icon: const Icon(Icons.clear, size: 22),
|
||||||
onPressed: _memberSearchCtr.onClear,
|
onPressed: _controller.onClear,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onSubmitted: (value) => _memberSearchCtr.submit(),
|
onSubmitted: (value) => _controller.submit(),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value.isEmpty) {
|
if (value.isEmpty) {
|
||||||
_memberSearchCtr.hasData.value = false;
|
_controller.hasData.value = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Obx(
|
body: SafeArea(
|
||||||
() => _memberSearchCtr.hasData.value
|
top: false,
|
||||||
? SafeArea(
|
bottom: false,
|
||||||
top: false,
|
child: Stack(
|
||||||
bottom: false,
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
Obx(() {
|
||||||
|
return Opacity(
|
||||||
|
opacity: _controller.hasData.value ? 1 : 0,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
TabBar(
|
TabBar(
|
||||||
controller: _memberSearchCtr.tabController,
|
controller: _controller.tabController,
|
||||||
tabs: [
|
tabs: [
|
||||||
Obx(
|
Obx(
|
||||||
() => Tab(
|
() => Tab(
|
||||||
text:
|
text:
|
||||||
'视频 ${_memberSearchCtr.archiveCount.value != -1 ? '${_memberSearchCtr.archiveCount.value}' : ''}',
|
'视频 ${_controller.counts[0] != -1 ? _controller.counts[0] : ''}',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Obx(
|
Obx(
|
||||||
() => Tab(
|
() => Tab(
|
||||||
text:
|
text:
|
||||||
'动态 ${_memberSearchCtr.dynamicCount.value != -1 ? '${_memberSearchCtr.dynamicCount.value}' : ''}',
|
'动态 ${_controller.counts[1] != -1 ? _controller.counts[1] : ''}',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: tabBarView(
|
child: tabBarView(
|
||||||
controller: _memberSearchCtr.tabController,
|
controller: _controller.tabController,
|
||||||
children: [
|
children: [
|
||||||
SearchArchive(ctr: _memberSearchCtr),
|
MemberSearchChildPage(
|
||||||
SearchDynamic(ctr: _memberSearchCtr),
|
controller: _controller.arcCtr,
|
||||||
|
searchType: MemberSearchType.archive,
|
||||||
|
),
|
||||||
|
MemberSearchChildPage(
|
||||||
|
controller: _controller.dynCtr,
|
||||||
|
searchType: MemberSearchType.dynamic,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
: FractionallySizedBox(
|
}),
|
||||||
heightFactor: 0.5,
|
Obx(
|
||||||
widthFactor: 1.0,
|
() => _controller.hasData.value
|
||||||
child: Center(
|
? const SizedBox.shrink()
|
||||||
child: Text(
|
: FractionallySizedBox(
|
||||||
'搜索「${_memberSearchCtr.uname.value}」的动态、视频',
|
heightFactor: 0.5,
|
||||||
textAlign: TextAlign.center,
|
widthFactor: 1.0,
|
||||||
),
|
child: Center(
|
||||||
),
|
child: Text(
|
||||||
),
|
'搜索「${_controller.uname}」的动态、视频',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user