mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-06-01 16:48:16 +08:00
opt pub page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
36
lib/pages/video/reply_search_item/child/controller.dart
Normal file
36
lib/pages/video/reply_search_item/child/controller.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||
show SearchItemReply, SearchItem, SearchItemType;
|
||||
import 'package:PiliPlus/grpc/reply.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/models/common/reply/reply_search_type.dart';
|
||||
import 'package:PiliPlus/pages/common/common_list_controller.dart';
|
||||
import 'package:PiliPlus/pages/video/reply_search_item/controller.dart';
|
||||
|
||||
class ReplySearchChildController
|
||||
extends CommonListController<SearchItemReply, SearchItem> {
|
||||
ReplySearchChildController(this.controller, this.searchType);
|
||||
|
||||
final ReplySearchController controller;
|
||||
final ReplySearchType searchType;
|
||||
|
||||
@override
|
||||
List<SearchItem>? getDataList(SearchItemReply response) {
|
||||
if (response.cursor.hasNext == false) {
|
||||
isEnd = true;
|
||||
}
|
||||
return response.items;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LoadingState<SearchItemReply>> customGetData() {
|
||||
return ReplyGrpc.searchItem(
|
||||
page: page,
|
||||
itemType: searchType == ReplySearchType.video
|
||||
? SearchItemType.VIDEO
|
||||
: SearchItemType.ARTICLE,
|
||||
oid: controller.oid,
|
||||
type: controller.type,
|
||||
keyword: controller.editingController.text,
|
||||
);
|
||||
}
|
||||
}
|
||||
94
lib/pages/video/reply_search_item/child/view.dart
Normal file
94
lib/pages/video/reply_search_item/child/view.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
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/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||
show SearchItem;
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/models/common/reply/reply_search_type.dart';
|
||||
import 'package:PiliPlus/pages/video/reply_search_item/child/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/reply_search_item/child/widgets/item.dart';
|
||||
import 'package:PiliPlus/utils/grid.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ReplySearchChildPage extends StatefulWidget {
|
||||
const ReplySearchChildPage({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.searchType,
|
||||
});
|
||||
|
||||
final ReplySearchChildController controller;
|
||||
final ReplySearchType searchType;
|
||||
|
||||
@override
|
||||
State<ReplySearchChildPage> createState() => _ReplySearchChildPageState();
|
||||
}
|
||||
|
||||
class _ReplySearchChildPageState extends State<ReplySearchChildPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
ReplySearchChildController get _controller => widget.controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return refreshIndicator(
|
||||
onRefresh: _controller.onRefresh,
|
||||
child: CustomScrollView(
|
||||
controller: _controller.scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 7,
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 80,
|
||||
),
|
||||
sliver: Obx(() => _buildBody(_controller.loadingState.value)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _buildLoading {
|
||||
return SliverGrid(
|
||||
gridDelegate: Grid.videoCardHDelegate(context),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return const VideoCardHSkeleton();
|
||||
},
|
||||
childCount: 10,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(LoadingState<List<SearchItem>?> loadingState) {
|
||||
return switch (loadingState) {
|
||||
Loading() => _buildLoading,
|
||||
Success(:var response) => response?.isNotEmpty == true
|
||||
? SliverGrid(
|
||||
gridDelegate: Grid.videoCardHDelegate(context),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (index == response.length - 1) {
|
||||
_controller.onLoadMore();
|
||||
}
|
||||
return ReplySearchItem(
|
||||
item: response[index],
|
||||
type: widget.searchType,
|
||||
);
|
||||
},
|
||||
childCount: response!.length,
|
||||
),
|
||||
)
|
||||
: HttpError(onReload: _controller.onReload),
|
||||
Error(:var errMsg) => HttpError(
|
||||
errMsg: errMsg,
|
||||
onReload: _controller.onReload,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
125
lib/pages/video/reply_search_item/child/widgets/item.dart
Normal file
125
lib/pages/video/reply_search_item/child/widgets/item.dart
Normal file
@@ -0,0 +1,125 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/badge.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/image_save.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||
show SearchItem, SearchItemVideoSubType;
|
||||
import 'package:PiliPlus/models/common/badge_type.dart';
|
||||
import 'package:PiliPlus/models/common/reply/reply_search_type.dart';
|
||||
import 'package:PiliPlus/utils/duration_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ReplySearchItem extends StatelessWidget {
|
||||
const ReplySearchItem({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
final SearchItem item;
|
||||
final ReplySearchType type;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String title = '';
|
||||
String cover = '';
|
||||
String? upNickname;
|
||||
String? category;
|
||||
int? duration;
|
||||
switch (type) {
|
||||
case ReplySearchType.video:
|
||||
if (item.video.type == SearchItemVideoSubType.UGC) {
|
||||
final ugc = item.video.ugc;
|
||||
title = ugc.title;
|
||||
cover = ugc.cover;
|
||||
upNickname = ugc.upNickname;
|
||||
duration = ugc.duration.toInt();
|
||||
} else {
|
||||
final pgc = item.video.pgc;
|
||||
title = pgc.title;
|
||||
cover = pgc.cover;
|
||||
category = pgc.category;
|
||||
}
|
||||
case ReplySearchType.article:
|
||||
final article = item.article;
|
||||
title = article.title;
|
||||
cover = article.covers.firstOrNull ?? '';
|
||||
upNickname = article.upNickname;
|
||||
}
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
onTap: () => Get.back(result: (title: title, url: item.url)),
|
||||
onLongPress: () => imageSaveDialog(
|
||||
title: title,
|
||||
cover: cover,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: StyleString.safeSpace,
|
||||
vertical: 5,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
return Stack(
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
src: cover,
|
||||
width: boxConstraints.maxWidth,
|
||||
height: boxConstraints.maxHeight,
|
||||
),
|
||||
if (category != null)
|
||||
PBadge(
|
||||
right: 6,
|
||||
top: 6,
|
||||
text: category,
|
||||
),
|
||||
if (duration != null)
|
||||
PBadge(
|
||||
right: 6,
|
||||
bottom: 6,
|
||||
text: DurationUtil.formatDuration(duration),
|
||||
type: PBadgeType.gray,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (upNickname != null)
|
||||
Text(
|
||||
'UP: $upNickname',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
56
lib/pages/video/reply_search_item/controller.dart
Normal file
56
lib/pages/video/reply_search_item/controller.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:PiliPlus/models/common/reply/reply_search_type.dart';
|
||||
import 'package:PiliPlus/pages/video/reply_search_item/child/controller.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ReplySearchController extends GetxController
|
||||
with GetSingleTickerProviderStateMixin {
|
||||
ReplySearchController(this.type, this.oid);
|
||||
final int type;
|
||||
final int oid;
|
||||
|
||||
late final tabController = TabController(vsync: this, length: 2);
|
||||
final editingController = TextEditingController();
|
||||
final focusNode = FocusNode();
|
||||
|
||||
late final videoCtr = Get.put(
|
||||
ReplySearchChildController(this, ReplySearchType.video),
|
||||
tag: Utils.generateRandomString(8));
|
||||
late final articleCtr = Get.put(
|
||||
ReplySearchChildController(this, ReplySearchType.article),
|
||||
tag: Utils.generateRandomString(8));
|
||||
|
||||
void onClear() {
|
||||
if (editingController.value.text.isNotEmpty) {
|
||||
editingController.clear();
|
||||
focusNode.requestFocus();
|
||||
} else {
|
||||
Get.back();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
submit();
|
||||
}
|
||||
|
||||
void submit() {
|
||||
videoCtr
|
||||
..scrollController.jumpToTop()
|
||||
..onReload();
|
||||
articleCtr
|
||||
..scrollController.jumpToTop()
|
||||
..onReload();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
editingController.dispose();
|
||||
focusNode.dispose();
|
||||
tabController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
99
lib/pages/video/reply_search_item/view.dart
Normal file
99
lib/pages/video/reply_search_item/view.dart
Normal file
@@ -0,0 +1,99 @@
|
||||
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
|
||||
import 'package:PiliPlus/models/common/reply/reply_search_type.dart';
|
||||
import 'package:PiliPlus/pages/video/reply_search_item/child/view.dart';
|
||||
import 'package:PiliPlus/pages/video/reply_search_item/controller.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ReplySearchPage extends StatefulWidget {
|
||||
const ReplySearchPage({
|
||||
super.key,
|
||||
required this.type,
|
||||
required this.oid,
|
||||
});
|
||||
|
||||
final int type;
|
||||
final int oid;
|
||||
|
||||
@override
|
||||
State<ReplySearchPage> createState() => _ReplySearchPageState();
|
||||
}
|
||||
|
||||
class _ReplySearchPageState extends State<ReplySearchPage> {
|
||||
late final _controller = Get.put(
|
||||
ReplySearchController(widget.type, widget.oid),
|
||||
tag: Utils.generateRandomString(8));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: '搜索',
|
||||
onPressed: _controller.submit,
|
||||
icon: const Icon(Icons.search, size: 22),
|
||||
),
|
||||
const SizedBox(width: 10)
|
||||
],
|
||||
title: TextField(
|
||||
autofocus: true,
|
||||
focusNode: _controller.focusNode,
|
||||
controller: _controller.editingController,
|
||||
textInputAction: TextInputAction.search,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
decoration: InputDecoration(
|
||||
hintText: '搜索',
|
||||
border: InputBorder.none,
|
||||
suffixIcon: IconButton(
|
||||
tooltip: '清空',
|
||||
icon: const Icon(Icons.clear, size: 22),
|
||||
onPressed: _controller.onClear,
|
||||
),
|
||||
),
|
||||
onSubmitted: (value) => _controller.submit(),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
controller: _controller.tabController,
|
||||
tabs: [
|
||||
const Tab(text: '视频'),
|
||||
const Tab(text: '专栏'),
|
||||
],
|
||||
onTap: (index) {
|
||||
if (!_controller.tabController.indexIsChanging) {
|
||||
if (index == 0) {
|
||||
_controller.videoCtr.animateToTop();
|
||||
} else {
|
||||
_controller.articleCtr.animateToTop();
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: tabBarView(
|
||||
controller: _controller.tabController,
|
||||
children: [
|
||||
ReplySearchChildPage(
|
||||
controller: _controller.videoCtr,
|
||||
searchType: ReplySearchType.video,
|
||||
),
|
||||
ReplySearchChildPage(
|
||||
controller: _controller.articleCtr,
|
||||
searchType: ReplySearchType.article,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user