mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
refa: reply2reply panel
tweaks Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomSliverPersistentHeaderDelegate
|
||||
@@ -26,7 +28,17 @@ class CustomSliverPersistentHeaderDelegate
|
||||
//overlapsContent:SliverPersistentHeader覆盖其他子组件返回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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取热搜关键词
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('已经是第一集了');
|
||||
|
||||
Reference in New Issue
Block a user