opt pub page

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-07-08 21:42:35 +08:00
parent 8bf55ec95a
commit 05153fda72
27 changed files with 1374 additions and 288 deletions

View 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,
);
}
}

View 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;
}

View 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,
),
),
],
),
),
],
),
),
),
);
}
}

View 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();
}
}

View 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,
),
],
),
),
],
),
),
);
}
}