refa: reply2reply panel

tweaks

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-09-13 14:26:08 +08:00
parent 0f2908dbc1
commit d3f4ba4b4a
16 changed files with 387 additions and 376 deletions

View File

@@ -1,3 +1,5 @@
import 'dart:io' show Platform;
import 'package:flutter/material.dart';
class CustomSliverPersistentHeaderDelegate
@@ -26,7 +28,17 @@ class CustomSliverPersistentHeaderDelegate
//overlapsContentSliverPersistentHeader覆盖其他子组件返回true否则返回false
return bgColor != null
? DecoratedBox(
decoration: BoxDecoration(color: bgColor),
decoration: BoxDecoration(
color: bgColor,
boxShadow: Platform.isIOS
? null
: [
BoxShadow(
color: bgColor!,
offset: const Offset(0, -1),
),
],
),
child: child,
)
: child;

View File

@@ -846,7 +846,9 @@ class _InteractiveViewerState extends State<InteractiveViewer>
_currentRotation = desiredRotation;
case _GestureType.pan:
assert(_referenceFocalPoint != null);
if (_referenceFocalPoint == null) {
return;
}
// details may have a change in scale here when scaleEnabled is false.
// In an effort to keep the behavior similar whether or not scaleEnabled
// is true, these gestures are thrown away.

View File

@@ -1,4 +1,4 @@
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
import 'package:PiliPlus/http/fav.dart';
import 'package:PiliPlus/http/msg.dart';
import 'package:PiliPlus/models_new/fav/fav_folder/list.dart';
@@ -101,17 +101,7 @@ class _CreateFavPageState extends State<CreateFavPage> {
? _titleController.text.isNotEmpty
? _buildBody(theme)
: _errMsg?.isNotEmpty == true
? Center(
child: CustomScrollView(
shrinkWrap: true,
slivers: [
HttpError(
errMsg: _errMsg,
onReload: _getFolderInfo,
),
],
),
)
? scrollErrorWidget(errMsg: _errMsg, onReload: _getFolderInfo)
: const Center(child: CircularProgressIndicator())
: _buildBody(theme),
);

View File

@@ -83,11 +83,13 @@ class _PgcReviewChildPageState extends State<PgcReviewChildPage>
);
return switch (loadingState) {
Loading() => SliverToBoxAdapter(
child: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => const VideoReplySkeleton(),
itemCount: 8,
child: IgnorePointer(
child: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => const VideoReplySkeleton(),
itemCount: 8,
),
),
),
Success(:var response) =>

View File

@@ -1,6 +1,7 @@
import 'dart:typed_data';
import 'dart:ui';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/button/icon_button.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
@@ -271,7 +272,7 @@ class _SavePanelState extends State<SavePanel> {
}
Future<void> _onSaveOrSharePic([bool isShare = false]) async {
if (!isShare) {
if (!isShare && Utils.isMobile) {
if (mounted && !await ImageUtils.checkPermissionDependOnSdkInt(context)) {
return;
}
@@ -285,7 +286,7 @@ class _SavePanelState extends State<SavePanel> {
ByteData? byteData = await image.toByteData(format: ImageByteFormat.png);
Uint8List pngBytes = byteData!.buffer.asUint8List();
String picName =
"plpl_reply_${DateFormat('yyyyMMddHHmmss').format(DateTime.now())}";
"${Constants.appName}_${DateFormat('yyyyMMddHHmmss').format(DateTime.now())}";
if (isShare) {
Get.back();
SmartDialog.dismiss();

View File

@@ -10,7 +10,9 @@ import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/id_utils.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:get/get.dart';
import 'package:stream_transform/stream_transform.dart';
@@ -165,6 +167,13 @@ class SSearchController extends GetxController
},
);
searchFocusNode.requestFocus();
if (Utils.isDesktop) {
SchedulerBinding.instance.addPostFrameCallback((_) {
controller.selection = TextSelection.collapsed(
offset: controller.text.length,
);
});
}
}
// 获取热搜关键词

View File

@@ -97,100 +97,98 @@ class _IntroDetailState extends CommonCollapseSlidePageState<PgcIntroPanel> {
final TextStyle textStyle = TextStyle(
color: theme.colorScheme.onSurfaceVariant,
);
return SelectionArea(
child: ListView(
controller: _controller,
physics: const AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.only(
left: 14,
right: 14,
top: 14,
bottom: MediaQuery.viewPaddingOf(context).bottom + 100,
),
children: [
Text(
widget.item.title!,
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 4),
Row(
spacing: 6,
children: [
StatWidget(
type: StatType.play,
value: widget.item.stat!.view,
),
StatWidget(
type: StatType.danmaku,
value: widget.item.stat!.danmaku,
),
],
),
const SizedBox(height: 4),
Row(
children: [
Text(
widget.item.areas!.first.name!,
style: smallTitle,
),
const SizedBox(width: 6),
Text(
widget.item.publish!.pubTimeShow!,
style: smallTitle,
),
const SizedBox(width: 6),
Text(
widget.item.newEp!.desc!,
style: smallTitle,
),
],
),
if (widget.item.evaluate?.isNotEmpty == true) ...[
const SizedBox(height: 20),
Text(
'简介:',
style: theme.textTheme.titleMedium,
),
const SizedBox(height: 4),
Text(
widget.item.evaluate!,
style: textStyle,
),
],
if (widget.item.actors?.isNotEmpty == true) ...[
const SizedBox(height: 20),
Text(
'演职人员:',
style: theme.textTheme.titleMedium,
),
const SizedBox(height: 4),
Text(
widget.item.actors!,
style: textStyle,
),
],
if (widget.videoTags?.isNotEmpty == true) ...[
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: widget.videoTags!
.map(
(item) => SearchText(
fontSize: 13,
text: item.tagName!,
onTap: (tagName) => Get.toNamed(
'/searchResult',
parameters: {'keyword': tagName},
),
onLongPress: Utils.copyText,
),
)
.toList(),
),
],
],
return ListView(
controller: _controller,
physics: const AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.only(
left: 14,
right: 14,
top: 14,
bottom: MediaQuery.viewPaddingOf(context).bottom + 100,
),
children: [
SelectableText(
widget.item.title!,
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 4),
Row(
spacing: 6,
children: [
StatWidget(
type: StatType.play,
value: widget.item.stat!.view,
),
StatWidget(
type: StatType.danmaku,
value: widget.item.stat!.danmaku,
),
],
),
const SizedBox(height: 4),
Row(
children: [
Text(
widget.item.areas!.first.name!,
style: smallTitle,
),
const SizedBox(width: 6),
Text(
widget.item.publish!.pubTimeShow!,
style: smallTitle,
),
const SizedBox(width: 6),
Text(
widget.item.newEp!.desc!,
style: smallTitle,
),
],
),
if (widget.item.evaluate?.isNotEmpty == true) ...[
const SizedBox(height: 20),
Text(
'简介:',
style: theme.textTheme.titleMedium,
),
const SizedBox(height: 4),
SelectableText(
widget.item.evaluate!,
style: textStyle,
),
],
if (widget.item.actors?.isNotEmpty == true) ...[
const SizedBox(height: 20),
Text(
'演职人员:',
style: theme.textTheme.titleMedium,
),
const SizedBox(height: 4),
Text(
widget.item.actors!,
style: textStyle,
),
],
if (widget.videoTags?.isNotEmpty == true) ...[
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: widget.videoTags!
.map(
(item) => SearchText(
fontSize: 13,
text: item.tagName!,
onTap: (tagName) => Get.toNamed(
'/searchResult',
parameters: {'keyword': tagName},
),
onLongPress: Utils.copyText,
),
)
.toList(),
),
],
],
);
}
}

View File

@@ -431,7 +431,11 @@ class UgcIntroController extends CommonIntroController with ReloadMixin {
SmartDialog.showToast('账号未登录');
return;
}
int? mid = videoDetail.value.owner?.mid;
final videoDetail = this.videoDetail.value;
if (videoDetail.staff?.isNotEmpty == true) {
return;
}
int? mid = videoDetail.owner?.mid;
if (mid == null) {
return;
}

View File

@@ -171,27 +171,13 @@ class _UgcIntroPanelState extends TripleState<UgcIntroPanel>
const SizedBox(height: 8),
if (isLoading)
_buildVideoTitle(theme, videoDetail)
else if (isHorizontal && Utils.isDesktop)
_buildTitle(theme, videoDetail, isExpand: true)
else
ExpandablePanel(
controller: introController.expandableCtr,
collapsed: GestureDetector(
onLongPress: () {
Feedback.forLongPress(context);
Utils.copyText(videoDetail.title ?? '');
},
child: _buildVideoTitle(theme, videoDetail),
),
expanded: GestureDetector(
onLongPress: () {
Feedback.forLongPress(context);
Utils.copyText(videoDetail.title ?? '');
},
child: _buildVideoTitle(
theme,
videoDetail,
isExpand: true,
),
),
collapsed: _buildTitle(theme, videoDetail),
expanded: _buildTitle(theme, videoDetail, isExpand: true),
theme: expandTheme,
),
const SizedBox(height: 8),
@@ -228,48 +214,19 @@ class _UgcIntroPanelState extends TripleState<UgcIntroPanel>
),
),
],
ExpandablePanel(
controller: introController.expandableCtr,
collapsed: const SizedBox.shrink(),
expanded: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
GestureDetector(
onTap: () => Utils.copyText('${videoDetail.bvid}'),
child: Text(
videoDetail.bvid ?? '',
style: TextStyle(
fontSize: 14,
color: theme.colorScheme.secondary,
),
),
),
if (videoDetail.descV2?.isNotEmpty == true) ...[
const SizedBox(height: 8),
SelectableText.rich(
style: const TextStyle(
height: 1.4,
),
TextSpan(
children: [
buildContent(theme, videoDetail),
],
),
),
],
Obx(() {
final videoTags = introController.videoTags.value;
if (videoTags.isNullOrEmpty) {
return const SizedBox.shrink();
}
return _buildTags(videoTags!);
}),
],
if (isHorizontal && Utils.isDesktop)
..._infos(theme, videoDetail)
else
ExpandablePanel(
controller: introController.expandableCtr,
collapsed: const SizedBox.shrink(),
expanded: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _infos(theme, videoDetail),
),
theme: expandTheme,
),
theme: expandTheme,
),
Obx(
() => introController.status.value
? const SizedBox.shrink()
@@ -339,6 +296,54 @@ class _UgcIntroPanelState extends TripleState<UgcIntroPanel>
);
}
Widget _buildTitle(
ThemeData theme,
VideoDetailData videoDetail, {
bool isExpand = false,
}) => GestureDetector(
onLongPress: () {
Feedback.forLongPress(context);
Utils.copyText(videoDetail.title ?? '');
},
child: _buildVideoTitle(
theme,
videoDetail,
isExpand: isExpand,
),
);
List<Widget> _infos(ThemeData theme, VideoDetailData videoDetail) => [
const SizedBox(height: 8),
GestureDetector(
onTap: () => Utils.copyText('${videoDetail.bvid}'),
child: Text(
videoDetail.bvid ?? '',
style: TextStyle(
fontSize: 14,
color: theme.colorScheme.secondary,
),
),
),
if (videoDetail.descV2?.isNotEmpty == true) ...[
const SizedBox(height: 8),
SelectableText.rich(
style: const TextStyle(height: 1.4),
TextSpan(
children: [
buildContent(theme, videoDetail),
],
),
),
],
Obx(() {
final videoTags = introController.videoTags.value;
if (videoTags.isNullOrEmpty) {
return const SizedBox.shrink();
}
return _buildTags(videoTags!);
}),
];
WidgetSpan _labelWidget(String text, Color bgColor, Color textColor) {
return WidgetSpan(
alignment: PlaceholderAlignment.middle,

View File

@@ -170,11 +170,13 @@ class _NoteListPageState extends CommonSlidePageState<NoteListPage> {
);
return switch (loadingState) {
Loading() => SliverToBoxAdapter(
child: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => const VideoReplySkeleton(),
itemCount: 8,
child: IgnorePointer(
child: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => const VideoReplySkeleton(),
itemCount: 8,
),
),
),
Success(:var response) =>

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/global_data.dart';
@@ -221,15 +222,17 @@ class _PayCoinsPageState extends State<PayCoinsPage>
@override
Widget build(BuildContext context) {
bool isPortrait = MediaQuery.sizeOf(context).isPortrait;
final size = MediaQuery.sizeOf(context);
final isPortrait = size.isPortrait;
return isPortrait
? _buildBody(isPortrait)
: Row(
children: [
const Spacer(),
Expanded(flex: 3, child: _buildBody(isPortrait)),
const Spacer(),
],
: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: math.min(525, size.width * 0.6),
),
child: _buildBody(isPortrait),
),
);
}

View File

@@ -11,7 +11,7 @@ import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_navigation/src/dialog/dialog_route.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
class VideoReplyReplyController extends ReplyController
with GetSingleTickerProviderStateMixin {
@@ -26,19 +26,19 @@ class VideoReplyReplyController extends ReplyController
});
final int? dialog;
final bool isDialogue;
final itemScrollCtr = ItemScrollController();
bool hasRoot = false;
int? id;
// 视频aid 请求时使用的oid
int oid;
// rpid 请求楼中楼回复
int rpid;
int replyType; // = ReplyType.video;
int replyType;
ReplyInfo? firstFloor;
bool hasRoot = false;
late final Rx<ReplyInfo?> firstFloor = Rx<ReplyInfo?>(null);
int? index;
AnimationController? controller;
AnimationController? animController;
final listController = ListController();
late final horizontalPreview = Pref.horizontalPreview;
@@ -69,27 +69,28 @@ class VideoReplyReplyController extends ReplyController
// reply2Reply // isDialogue.not
if (data is DetailListReply) {
count.value = data.root.count.toInt();
if (isRefresh && firstFloor == null) {
firstFloor = data.root;
if (isRefresh && !hasRoot) {
firstFloor.value ??= data.root;
}
if (id != null) {
final id64 = Int64(id!);
final index = data.root.replies.indexWhere((item) => item.id == id64);
if (index != -1) {
this.index = index;
controller = AnimationController(
animController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
WidgetsBinding.instance.addPostFrameCallback((_) async {
try {
itemScrollCtr.jumpTo(
index: hasRoot ? index + 3 : index + 1,
listController.jumpToItem(
index: index,
scrollController: scrollController,
alignment: 0.25,
);
await Future.delayed(
const Duration(milliseconds: 800),
controller?.forward,
animController?.forward,
);
this.index = null;
} catch (_) {}
@@ -200,7 +201,8 @@ class VideoReplyReplyController extends ReplyController
@override
void onClose() {
controller?.dispose();
animController?.dispose();
listController.dispose();
super.dispose();
}
}

View File

@@ -1,5 +1,6 @@
import 'package:PiliPlus/common/skeleton/video_reply.dart';
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.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 ReplyInfo, Mode;
@@ -13,7 +14,7 @@ import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart' hide ContextExtensionss;
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
class VideoReplyReplyPanel extends CommonSlidePage {
const VideoReplyReplyPanel({
@@ -48,15 +49,11 @@ class VideoReplyReplyPanel extends CommonSlidePage {
class _VideoReplyReplyPanelState
extends CommonSlidePageState<VideoReplyReplyPanel> {
late VideoReplyReplyController _controller;
late final itemPositionsListener = ItemPositionsListener.create();
late final _key = GlobalKey<ScaffoldState>();
late final _listKey = GlobalKey();
late final _tag = Utils.makeHeroTag(
'${widget.rpid}${widget.dialog}${widget.isDialogue}',
);
ReplyInfo? get firstFloor => widget.firstFloor ?? _controller.firstFloor;
bool get _horizontalPreview =>
_controller.horizontalPreview && context.isLandscape;
Function(List<String> imgList, int index)? _imageCallback;
@@ -138,163 +135,138 @@ class _VideoReplyReplyPanelState
);
}
ReplyInfo? get firstFloor =>
widget.firstFloor ?? _controller.firstFloor.value;
@override
Widget buildList(ThemeData theme) {
return refreshIndicator(
onRefresh: _controller.onRefresh,
child: Obx(
() => Stack(
clipBehavior: Clip.none,
children: [
ScrollablePositionedList.builder(
key: _listKey,
itemPositionsListener: itemPositionsListener,
itemCount: _itemCount(_controller.loadingState.value),
itemScrollController: _controller.itemScrollCtr,
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) {
if (widget.isDialogue) {
return _buildBody(
theme,
_controller.loadingState.value,
index,
);
} else if (firstFloor != null) {
if (index == 0) {
return ReplyItemGrpc(
replyItem: firstFloor!,
replyLevel: 2,
needDivider: false,
onReply: (replyItem) => _controller.onReply(
context,
replyItem: replyItem,
index: -1,
),
upMid: _controller.upMid,
onViewImage: widget.onViewImage,
onDismissed: widget.onDismissed,
callback: _imageCallback,
onCheckReply: (item) =>
_controller.onCheckReply(item, isManual: true),
);
} else if (index == 1) {
return Divider(
height: 20,
color: theme.dividerColor.withValues(alpha: 0.1),
thickness: 6,
);
} else if (index == 2) {
return _sortWidget(theme);
} else {
return _buildBody(
theme,
_controller.loadingState.value,
index - 3,
);
}
} else {
if (index == 0) {
return _sortWidget(theme);
} else {
return _buildBody(
theme,
_controller.loadingState.value,
index - 1,
);
}
child: CustomScrollView(
controller: _controller.scrollController,
slivers: [
if (!widget.isDialogue) ...[
if (widget.firstFloor case final firstFloor?)
_header(theme, firstFloor)
else
Obx(() {
final firstFloor = _controller.firstFloor.value;
if (firstFloor == null) {
return const SliverToBoxAdapter();
}
},
),
if (!widget.isDialogue && _controller.loadingState.value.isSuccess)
_header(theme),
return _header(theme, firstFloor);
}),
_sortWidget(theme),
],
Obx(() => _buildBody(theme, _controller.loadingState.value)),
],
),
);
}
Widget _header(ThemeData theme, ReplyInfo firstFloor) {
return SliverMainAxisGroup(
slivers: [
SliverToBoxAdapter(
child: ReplyItemGrpc(
replyItem: firstFloor,
replyLevel: 2,
needDivider: false,
onReply: (replyItem) => _controller.onReply(
context,
replyItem: replyItem,
index: -1,
),
upMid: _controller.upMid,
onViewImage: widget.onViewImage,
onDismissed: widget.onDismissed,
callback: _imageCallback,
onCheckReply: (item) =>
_controller.onCheckReply(item, isManual: true),
),
),
SliverToBoxAdapter(
child: Divider(
height: 20,
color: theme.dividerColor.withValues(alpha: 0.1),
thickness: 6,
),
),
],
);
}
Widget _sortWidget(ThemeData theme) {
return SliverPersistentHeader(
pinned: true,
delegate: CustomSliverPersistentHeaderDelegate(
extent: 40,
bgColor: theme.colorScheme.surface,
child: Container(
height: 40,
padding: const EdgeInsets.fromLTRB(12, 0, 6, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Obx(
() {
final count = _controller.count.value;
return count != -1
? Text(
'相关回复共${NumUtils.numFormat(count)}',
style: const TextStyle(fontSize: 13),
)
: const SizedBox.shrink();
},
),
SizedBox(
height: 35,
child: TextButton.icon(
onPressed: _controller.queryBySort,
icon: Icon(
Icons.sort,
size: 16,
color: theme.colorScheme.secondary,
),
label: Obx(
() => Text(
_controller.mode.value == Mode.MAIN_LIST_HOT
? '按热度'
: '按时间',
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.secondary,
),
),
),
),
),
],
),
),
),
);
}
Widget _header(ThemeData theme) => firstFloor == null
? _sortWidget(theme)
: ValueListenableBuilder<Iterable<ItemPosition>>(
valueListenable: itemPositionsListener.itemPositions,
builder: (context, positions, child) {
int min = -1;
if (positions.isNotEmpty) {
min = positions
.where(
(ItemPosition position) => position.itemTrailingEdge > 0,
)
.reduce(
(ItemPosition min, ItemPosition position) =>
position.itemTrailingEdge < min.itemTrailingEdge
? position
: min,
)
.index;
}
return min >= 2 ? _sortWidget(theme) : const SizedBox.shrink();
},
);
Widget _sortWidget(ThemeData theme) => Container(
height: 40,
color: theme.colorScheme.surface,
padding: const EdgeInsets.fromLTRB(12, 0, 6, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Obx(
() {
final count = _controller.count.value;
return count != -1
? Text(
'相关回复共${NumUtils.numFormat(count)}',
style: const TextStyle(fontSize: 13),
)
: const SizedBox.shrink();
},
),
SizedBox(
height: 35,
child: TextButton.icon(
onPressed: _controller.queryBySort,
icon: Icon(
Icons.sort,
size: 16,
color: theme.colorScheme.secondary,
),
label: Obx(
() => Text(
_controller.mode.value == Mode.MAIN_LIST_HOT ? '按热度' : '按时间',
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.secondary,
),
),
),
),
),
],
),
);
Widget _buildBody(
ThemeData theme,
LoadingState<List<ReplyInfo>?> loadingState,
int index,
) {
return switch (loadingState) {
Loading() => IgnorePointer(
child: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => const VideoReplySkeleton(),
itemCount: 8,
Loading() => SliverToBoxAdapter(
child: IgnorePointer(
child: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => const VideoReplySkeleton(),
itemCount: 8,
),
),
),
Success(:var response) => Builder(
builder: (context) {
if (index == response!.length) {
Success(:var response) => SuperSliverList.builder(
listController: _controller.listController,
itemBuilder: (context, index) {
if (index == response.length) {
_controller.onLoadMore();
return Container(
height: 125,
@@ -311,30 +283,30 @@ class _VideoReplyReplyPanelState
),
),
);
} else {
final child = _replyItem(response[index], index);
if (_controller.index != null && _controller.index == index) {
colorAnimation ??= ColorTween(
begin: theme.colorScheme.onInverseSurface,
end: theme.colorScheme.surface,
).animate(_controller.controller!);
return AnimatedBuilder(
animation: colorAnimation!,
builder: (context, _) {
return ColoredBox(
color:
colorAnimation!.value ??
theme.colorScheme.onInverseSurface,
child: child,
);
},
);
}
return child;
}
final child = _replyItem(response[index], index);
if (_controller.index == index) {
colorAnimation ??= ColorTween(
begin: theme.colorScheme.onInverseSurface,
end: theme.colorScheme.surface,
).animate(_controller.animController!);
return AnimatedBuilder(
animation: colorAnimation!,
builder: (context, _) {
return ColoredBox(
color:
colorAnimation!.value ??
theme.colorScheme.onInverseSurface,
child: child,
);
},
);
}
return child;
},
itemCount: response!.length + 1,
),
Error(:var errMsg) => errorWidget(
Error(:var errMsg) => HttpError(
errMsg: errMsg,
onReload: _controller.onReload,
),
@@ -367,15 +339,4 @@ class _VideoReplyReplyPanelState
onCheckReply: (item) => _controller.onCheckReply(item, isManual: true),
);
}
int _itemCount(LoadingState<List<ReplyInfo>?> loadingState) {
if (widget.isDialogue) {
return (loadingState.dataOrNull?.length ?? 0) + 1;
}
int itemCount = 0;
if (firstFloor != null) {
itemCount = 2;
}
return (loadingState.dataOrNull?.length ?? 0) + itemCount + 2;
}
}

View File

@@ -23,6 +23,7 @@ import 'package:PiliPlus/models_new/video/video_shot/data.dart';
import 'package:PiliPlus/pages/common/common_intro_controller.dart';
import 'package:PiliPlus/pages/video/controller.dart';
import 'package:PiliPlus/pages/video/introduction/pgc/controller.dart';
import 'package:PiliPlus/pages/video/introduction/ugc/controller.dart';
import 'package:PiliPlus/pages/video/post_panel/popup_menu_text.dart';
import 'package:PiliPlus/pages/video/post_panel/view.dart';
import 'package:PiliPlus/plugin/pl_player/controller.dart';
@@ -1203,6 +1204,12 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
introController.viewLater();
break;
case LogicalKeyboardKey.keyG when (!plPlayerController.isLive):
if (introController case UgcIntroController ugcCtr) {
ugcCtr.actionRelationMod(context);
}
break;
case LogicalKeyboardKey.bracketLeft when (!plPlayerController.isLive):
if (!introController.prevPlay()) {
SmartDialog.showToast('已经是第一集了');