mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-07-08 18:20:12 +08:00
tweaks (#1252)
* opt: cache * opt: MediaListPanel * feat: nested replyreply panel * tweaks * opt: abstract class * opt: PageStorageKey * opt: contextExt * opt: EpisodePanel * opt * opt: context instead GlobalKey * feat: jump to reply * refa: reply_reply * fix: jump * fix: index * update Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> * opt: keepalive * reapply: nested replyreply * mod: spacing * opt: CommonSlidePageState * fix drag bottomsheet Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> * opt reply jump Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> * opt reply2reply Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> * tweaks Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> * tweaks Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> * reapply: jumpToReply * fix: padding * fix: anim * fix some panels Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> * opt: implements Scaffold * opt: remove keepalive * revert: GlobalKey * tweaks Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> --------- Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
committed by
GitHub
parent
b9a55ccbce
commit
37fb63c3b1
@@ -1,12 +1,12 @@
|
||||
import 'package:PiliPlus/models_new/video/video_ai_conclusion/model_result.dart';
|
||||
import 'package:PiliPlus/pages/common/slide/common_collapse_slide_page.dart';
|
||||
import 'package:PiliPlus/pages/common/slide/common_slide_page.dart';
|
||||
import 'package:PiliPlus/pages/video/controller.dart';
|
||||
import 'package:PiliPlus/utils/duration_utils.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class AiConclusionPanel extends CommonCollapseSlidePage {
|
||||
class AiConclusionPanel extends CommonSlidePage {
|
||||
final AiConclusionResult item;
|
||||
|
||||
const AiConclusionPanel({
|
||||
@@ -18,15 +18,8 @@ class AiConclusionPanel extends CommonCollapseSlidePage {
|
||||
State<AiConclusionPanel> createState() => _AiDetailState();
|
||||
}
|
||||
|
||||
class _AiDetailState extends CommonCollapseSlidePageState<AiConclusionPanel> {
|
||||
final _controller = ScrollController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
class _AiDetailState extends State<AiConclusionPanel>
|
||||
with SingleTickerProviderStateMixin, CommonSlideMixin {
|
||||
@override
|
||||
Widget buildPage(ThemeData theme) {
|
||||
return Material(
|
||||
@@ -58,10 +51,18 @@ class _AiDetailState extends CommonCollapseSlidePageState<AiConclusionPanel> {
|
||||
);
|
||||
}
|
||||
|
||||
late Key _key;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_key = ValueKey(PrimaryScrollController.of(context).hashCode);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildList(ThemeData theme) {
|
||||
return CustomScrollView(
|
||||
controller: _controller,
|
||||
key: _key,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
if (widget.item.summary?.isNotEmpty == true) ...[
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:ui';
|
||||
|
||||
import 'package:PiliPlus/common/widgets/pair.dart';
|
||||
import 'package:PiliPlus/common/widgets/progress_bar/segment_progress_bar.dart';
|
||||
import 'package:PiliPlus/common/widgets/scaffold/scaffold.dart';
|
||||
import 'package:PiliPlus/http/constants.dart';
|
||||
import 'package:PiliPlus/http/fav.dart';
|
||||
import 'package:PiliPlus/http/init.dart';
|
||||
@@ -53,7 +54,7 @@ import 'package:dio/dio.dart' show Options;
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
||||
import 'package:flutter/foundation.dart' show kDebugMode;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart' hide Scaffold, ScaffoldState;
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:flutter_volume_controller/flutter_volume_controller.dart';
|
||||
import 'package:get/get.dart' hide ContextExtensionss;
|
||||
@@ -102,7 +103,6 @@ class VideoDetailController extends GetxController
|
||||
// 是否开始自动播放 存在多p的情况下,第二p需要为true
|
||||
final RxBool autoPlay = true.obs;
|
||||
|
||||
final scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
final childKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
PlPlayerController plPlayerController = PlPlayerController.getInstance()
|
||||
@@ -154,9 +154,10 @@ class VideoDetailController extends GetxController
|
||||
late double videoHeight;
|
||||
|
||||
void animToTop() {
|
||||
if (scrollKey.currentState?.outerController.hasClients == true) {
|
||||
scrollKey.currentState!.outerController.animateTo(
|
||||
scrollKey.currentState!.outerController.offset,
|
||||
final outerController = scrollKey.currentState!.outerController;
|
||||
if (outerController.hasClients) {
|
||||
outerController.animateTo(
|
||||
outerController.offset,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
@@ -361,7 +362,7 @@ class VideoDetailController extends GetxController
|
||||
} catch (_) {}
|
||||
},
|
||||
panelTitle: watchLaterTitle,
|
||||
getBvId: () => bvid,
|
||||
bvid: bvid,
|
||||
count: args['count'],
|
||||
loadMoreMedia: getMediaList,
|
||||
desc: _mediaDesc,
|
||||
@@ -1508,8 +1509,8 @@ class VideoDetailController extends GetxController
|
||||
idx = subtitles.indexWhere((i) => !i.lan!.startsWith('ai')) + 1;
|
||||
if (idx == 0) {
|
||||
if (preference == SubtitlePrefType.on ||
|
||||
(preference == SubtitlePrefType.auto &&
|
||||
Utils.isMobile &&
|
||||
(Utils.isMobile &&
|
||||
preference == SubtitlePrefType.auto &&
|
||||
(await FlutterVolumeController.getVolume() ?? 0) <= 0)) {
|
||||
idx = 1;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import 'package:PiliPlus/pages/video/introduction/pgc/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/introduction/pgc/widgets/pgc_panel.dart';
|
||||
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/action_item.dart';
|
||||
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/triple_state.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/num_utils.dart';
|
||||
import 'package:PiliPlus/utils/page_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -43,25 +44,23 @@ class PgcIntroPage extends StatefulWidget {
|
||||
State<PgcIntroPage> createState() => _PgcIntroPageState();
|
||||
}
|
||||
|
||||
class _PgcIntroPageState extends TripleState<PgcIntroPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
class _PgcIntroPageState extends TripleState<PgcIntroPage> {
|
||||
@override
|
||||
late PgcIntroController introController;
|
||||
late VideoDetailController videoDetailCtr;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
late final PgcIntroController introController;
|
||||
late final VideoDetailController videoDetailCtr;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
introController = Get.put(PgcIntroController(), tag: widget.heroTag);
|
||||
introController = Get.putOrFind(
|
||||
PgcIntroController.new,
|
||||
tag: widget.heroTag,
|
||||
);
|
||||
videoDetailCtr = Get.find<VideoDetailController>(tag: widget.heroTag);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final item = introController.pgcItem;
|
||||
final isLandscape = widget.isLandscape;
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:PiliPlus/common/widgets/stat/stat.dart';
|
||||
import 'package:PiliPlus/models/common/stat_type.dart';
|
||||
import 'package:PiliPlus/models_new/pgc/pgc_info_model/result.dart';
|
||||
import 'package:PiliPlus/models_new/video/video_tag/data.dart';
|
||||
import 'package:PiliPlus/pages/common/slide/common_collapse_slide_page.dart';
|
||||
import 'package:PiliPlus/pages/common/slide/common_slide_page.dart';
|
||||
import 'package:PiliPlus/pages/pgc_review/view.dart';
|
||||
import 'package:PiliPlus/pages/search/widgets/search_text.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
@@ -14,7 +14,7 @@ import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart' hide TabBarView;
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class PgcIntroPanel extends CommonCollapseSlidePage {
|
||||
class PgcIntroPanel extends CommonSlidePage {
|
||||
final PgcInfoModel item;
|
||||
final List<VideoTagItem>? videoTags;
|
||||
|
||||
@@ -29,7 +29,8 @@ class PgcIntroPanel extends CommonCollapseSlidePage {
|
||||
State<PgcIntroPanel> createState() => _IntroDetailState();
|
||||
}
|
||||
|
||||
class _IntroDetailState extends CommonCollapseSlidePageState<PgcIntroPanel> {
|
||||
class _IntroDetailState extends State<PgcIntroPanel>
|
||||
with TickerProviderStateMixin, CommonSlideMixin {
|
||||
late final _tabController = TabController(length: 2, vsync: this);
|
||||
final _controller = ScrollController();
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:PiliPlus/models/user/info.dart';
|
||||
import 'package:PiliPlus/models_new/pgc/pgc_info_model/episode.dart';
|
||||
import 'package:PiliPlus/models_new/pgc/pgc_info_model/new_ep.dart';
|
||||
import 'package:PiliPlus/models_new/video/video_detail/episode.dart'
|
||||
@@ -37,22 +36,23 @@ class PgcPanel extends StatefulWidget {
|
||||
|
||||
class _PgcPanelState extends State<PgcPanel> {
|
||||
late int currentIndex;
|
||||
final ScrollController listViewScrollCtr = ScrollController();
|
||||
late final ScrollController listViewScrollCtr;
|
||||
// 默认未开通
|
||||
late int vipStatus;
|
||||
late final bool vipStatus;
|
||||
late int cid;
|
||||
late final VideoDetailController videoDetailCtr;
|
||||
StreamSubscription? _listener;
|
||||
late final StreamSubscription<int> _listener;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
cid = widget.cid!;
|
||||
currentIndex = widget.pages.indexWhere((e) => e.cid == cid);
|
||||
scrollToIndex();
|
||||
listViewScrollCtr = ScrollController(
|
||||
initialScrollOffset: currentIndex * 150.0,
|
||||
);
|
||||
|
||||
UserInfoData? userInfo = Pref.userInfoCache;
|
||||
vipStatus = userInfo?.vipStatus ?? 0;
|
||||
vipStatus = Pref.userInfoCache?.vipStatus != 1;
|
||||
|
||||
videoDetailCtr = Get.find<VideoDetailController>(tag: widget.heroTag);
|
||||
|
||||
@@ -67,7 +67,7 @@ class _PgcPanelState extends State<PgcPanel> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_listener?.cancel();
|
||||
_listener.cancel();
|
||||
listViewScrollCtr.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -134,6 +134,7 @@ class _PgcPanelState extends State<PgcPanel> {
|
||||
SizedBox(
|
||||
height: 60,
|
||||
child: ListView.builder(
|
||||
key: const PageStorageKey(_PgcPanelState),
|
||||
padding: EdgeInsets.zero,
|
||||
controller: listViewScrollCtr,
|
||||
scrollDirection: Axis.horizontal,
|
||||
@@ -163,7 +164,7 @@ class _PgcPanelState extends State<PgcPanel> {
|
||||
child: InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
onTap: () {
|
||||
if (item.badge == '会员' && vipStatus != 1) {
|
||||
if (item.badge == '会员' && vipStatus) {
|
||||
SmartDialog.showToast('需要大会员');
|
||||
}
|
||||
widget.onChangeEpisode(item);
|
||||
|
||||
@@ -22,7 +22,6 @@ import 'package:PiliPlus/models_new/video/video_detail/section.dart';
|
||||
import 'package:PiliPlus/models_new/video/video_detail/staff.dart';
|
||||
import 'package:PiliPlus/models_new/video/video_detail/stat_detail.dart';
|
||||
import 'package:PiliPlus/models_new/video/video_detail/ugc_season.dart';
|
||||
import 'package:PiliPlus/models_new/video/video_relation/data.dart';
|
||||
import 'package:PiliPlus/pages/common/common_intro_controller.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_repost/view.dart';
|
||||
import 'package:PiliPlus/pages/video/controller.dart';
|
||||
@@ -166,19 +165,18 @@ class UgcIntroController extends CommonIntroController with ReloadMixin {
|
||||
|
||||
Future<void> queryAllStatus() async {
|
||||
var result = await VideoHttp.videoRelation(bvid: bvid);
|
||||
if (result['status']) {
|
||||
VideoRelation data = result['data'];
|
||||
if (result case Success(:var response)) {
|
||||
late final stat = videoDetail.value.stat!;
|
||||
if (data.like!) {
|
||||
if (response.like!) {
|
||||
stat.like = max(1, stat.like);
|
||||
}
|
||||
if (data.favorite!) {
|
||||
if (response.favorite!) {
|
||||
stat.favorite = max(1, stat.favorite);
|
||||
}
|
||||
hasLike.value = data.like!;
|
||||
hasDislike.value = data.dislike!;
|
||||
coinNum.value = data.coin!;
|
||||
hasFav.value = data.favorite!;
|
||||
hasLike.value = response.like!;
|
||||
hasDislike.value = response.dislike!;
|
||||
coinNum.value = response.coin!;
|
||||
hasFav.value = response.favorite!;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,22 +54,23 @@ class UgcIntroPanel extends StatefulWidget {
|
||||
State<UgcIntroPanel> createState() => _UgcIntroPanelState();
|
||||
}
|
||||
|
||||
class _UgcIntroPanelState extends TripleState<UgcIntroPanel>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
class _UgcIntroPanelState extends TripleState<UgcIntroPanel> {
|
||||
@override
|
||||
late UgcIntroController introController;
|
||||
late final UgcIntroController introController;
|
||||
late final VideoDetailController videoDetailCtr =
|
||||
Get.find<VideoDetailController>(tag: widget.heroTag);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
introController = Get.put(UgcIntroController(), tag: widget.heroTag);
|
||||
introController = Get.putOrFind(
|
||||
UgcIntroController.new,
|
||||
tag: widget.heroTag,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
final ThemeData theme = Theme.of(context);
|
||||
const expandTheme = ExpandableThemeData(
|
||||
animationDuration: Duration(milliseconds: 300),
|
||||
@@ -963,7 +964,4 @@ class _UgcIntroPanelState extends TripleState<UgcIntroPanel>
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ class PagesPanel extends StatefulWidget {
|
||||
class _PagesPanelState extends State<PagesPanel> {
|
||||
late int cid;
|
||||
int pageIndex = -1;
|
||||
late VideoDetailController _videoDetailController;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
late final VideoDetailController _videoDetailController;
|
||||
late final ScrollController _scrollController;
|
||||
StreamSubscription? _listener;
|
||||
|
||||
List<Part> get pages =>
|
||||
@@ -48,28 +48,32 @@ class _PagesPanelState extends State<PagesPanel> {
|
||||
_videoDetailController = Get.find<VideoDetailController>(
|
||||
tag: widget.heroTag,
|
||||
);
|
||||
double offset = 0;
|
||||
if (widget.list == null) {
|
||||
cid = widget.ugcIntroController.cid.value;
|
||||
pageIndex = pages.indexWhere((Part e) => e.cid == cid);
|
||||
_listener = _videoDetailController.cid.listen((int cid) {
|
||||
offset = targetOffset;
|
||||
_listener = _videoDetailController.cid.listen((cid) {
|
||||
this.cid = cid;
|
||||
pageIndex = max(0, pages.indexWhere((Part e) => e.cid == cid));
|
||||
pageIndex = max(0, pages.indexWhere((e) => e.cid == cid));
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
jumpToCurr();
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
jumpToCurr();
|
||||
});
|
||||
}
|
||||
_scrollController = ScrollController(initialScrollOffset: offset);
|
||||
}
|
||||
|
||||
double get targetOffset {
|
||||
const double itemWidth = 150;
|
||||
return max(0, pageIndex * itemWidth - itemWidth / 2);
|
||||
}
|
||||
|
||||
void jumpToCurr() {
|
||||
if (!_scrollController.hasClients || pages.isEmpty) {
|
||||
return;
|
||||
}
|
||||
const double itemWidth = 150;
|
||||
final double targetOffset = (pageIndex * itemWidth - itemWidth / 2).clamp(
|
||||
final double targetOffset = this.targetOffset.clamp(
|
||||
_scrollController.position.minScrollExtent,
|
||||
_scrollController.position.maxScrollExtent,
|
||||
);
|
||||
@@ -136,6 +140,7 @@ class _PagesPanelState extends State<PagesPanel> {
|
||||
SizedBox(
|
||||
height: 35,
|
||||
child: ListView.builder(
|
||||
key: PageStorageKey(widget.bvid),
|
||||
controller: _scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: pages.length,
|
||||
|
||||
@@ -9,21 +9,20 @@ import 'package:PiliPlus/models/common/badge_type.dart';
|
||||
import 'package:PiliPlus/models/common/stat_type.dart';
|
||||
import 'package:PiliPlus/models_new/media_list/media_list.dart';
|
||||
import 'package:PiliPlus/models_new/video/video_detail/episode.dart';
|
||||
import 'package:PiliPlus/pages/common/slide/common_collapse_slide_page.dart';
|
||||
import 'package:PiliPlus/pages/common/slide/common_slide_page.dart';
|
||||
import 'package:PiliPlus/utils/duration_utils.dart';
|
||||
import 'package:flutter/material.dart' hide RefreshCallback;
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
class MediaListPanel extends CommonCollapseSlidePage {
|
||||
class MediaListPanel extends CommonSlidePage {
|
||||
const MediaListPanel({
|
||||
super.key,
|
||||
required this.mediaList,
|
||||
required this.onChangeEpisode,
|
||||
this.panelTitle,
|
||||
required this.getBvId,
|
||||
required this.bvid,
|
||||
required this.loadMoreMedia,
|
||||
required this.count,
|
||||
required this.desc,
|
||||
@@ -32,37 +31,33 @@ class MediaListPanel extends CommonCollapseSlidePage {
|
||||
this.onDelete,
|
||||
});
|
||||
|
||||
final List<MediaListItemModel> mediaList;
|
||||
final RxList<MediaListItemModel> mediaList;
|
||||
final ValueChanged<BaseEpisodeItem> onChangeEpisode;
|
||||
final String? panelTitle;
|
||||
final Function getBvId;
|
||||
final String bvid;
|
||||
final VoidCallback loadMoreMedia;
|
||||
final int? count;
|
||||
final bool desc;
|
||||
final VoidCallback onReverse;
|
||||
final RefreshCallback? loadPrevious;
|
||||
final Function(MediaListItemModel item, int index)? onDelete;
|
||||
final void Function(MediaListItemModel item, int index)? onDelete;
|
||||
|
||||
@override
|
||||
State<MediaListPanel> createState() => _MediaListPanelState();
|
||||
}
|
||||
|
||||
class _MediaListPanelState
|
||||
extends CommonCollapseSlidePageState<MediaListPanel> {
|
||||
final _controller = ItemScrollController();
|
||||
class _MediaListPanelState extends State<MediaListPanel>
|
||||
with SingleTickerProviderStateMixin, CommonSlideMixin {
|
||||
late final ScrollController _controller;
|
||||
|
||||
@override
|
||||
void init() {
|
||||
final bvid = widget.getBvId();
|
||||
void initState() {
|
||||
super.initState();
|
||||
final bvid = widget.bvid;
|
||||
final bvIndex = widget.mediaList.indexWhere((item) => item.bvid == bvid);
|
||||
if (bvIndex != -1) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
isInit = false;
|
||||
_controller.jumpTo(index: bvIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
_controller = ScrollController(
|
||||
initialScrollOffset: bvIndex == -1 ? 0 : bvIndex * 100.0 + 7,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -120,31 +115,37 @@ class _MediaListPanelState
|
||||
: _buildList(theme);
|
||||
}
|
||||
|
||||
Widget _buildList(ThemeData theme) => Obx(
|
||||
() {
|
||||
final showDelBtn = widget.onDelete != null && widget.mediaList.length > 1;
|
||||
return ScrollablePositionedList.separated(
|
||||
itemScrollController: _controller,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemCount: widget.mediaList.length,
|
||||
padding: EdgeInsets.only(
|
||||
top: 7,
|
||||
bottom: MediaQuery.viewPaddingOf(context).bottom + 100,
|
||||
Widget _buildList(ThemeData theme) {
|
||||
final showDelBtn = widget.onDelete != null && widget.mediaList.length > 1;
|
||||
return CustomScrollView(
|
||||
controller: _controller,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 7,
|
||||
bottom: MediaQuery.viewPaddingOf(context).bottom + 100,
|
||||
),
|
||||
sliver: Obx(
|
||||
() => SliverFixedExtentList.builder(
|
||||
itemExtent: 100,
|
||||
itemCount: widget.mediaList.length,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == widget.mediaList.length - 1 &&
|
||||
(widget.count == null ||
|
||||
widget.mediaList.length < widget.count!)) {
|
||||
widget.loadMoreMedia();
|
||||
}
|
||||
var item = widget.mediaList[index];
|
||||
final isCurr = item.bvid == widget.bvid;
|
||||
return _buildItem(theme, index, item, isCurr, showDelBtn);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
itemBuilder: ((context, index) {
|
||||
if (index == widget.mediaList.length - 1 &&
|
||||
(widget.count == null ||
|
||||
widget.mediaList.length < widget.count!)) {
|
||||
widget.loadMoreMedia();
|
||||
}
|
||||
var item = widget.mediaList[index];
|
||||
final isCurr = item.bvid == widget.getBvId();
|
||||
return _buildItem(theme, index, item, isCurr, showDelBtn);
|
||||
}),
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 2),
|
||||
);
|
||||
},
|
||||
);
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItem(
|
||||
ThemeData theme,
|
||||
@@ -153,146 +154,151 @@ class _MediaListPanelState
|
||||
bool isCurr,
|
||||
bool showDelBtn,
|
||||
) {
|
||||
return SizedBox(
|
||||
height: 98,
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (item.type != 2) {
|
||||
SmartDialog.showToast('不支持播放该类型视频');
|
||||
return;
|
||||
}
|
||||
Get.back();
|
||||
widget.onChangeEpisode(item);
|
||||
},
|
||||
onLongPress: () => imageSaveDialog(
|
||||
title: item.title,
|
||||
cover: item.cover,
|
||||
aid: item.aid,
|
||||
bvid: item.bvid,
|
||||
),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 5,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
src: item.cover,
|
||||
width: 140.8,
|
||||
height: 88,
|
||||
),
|
||||
if (item.badge?.isNotEmpty == true)
|
||||
PBadge(
|
||||
text: item.badge,
|
||||
right: 6.0,
|
||||
top: 6.0,
|
||||
type: switch (item.badge) {
|
||||
'充电专属' => PBadgeType.error,
|
||||
_ => PBadgeType.primary,
|
||||
},
|
||||
),
|
||||
PBadge(
|
||||
text: DurationUtils.formatDuration(
|
||||
item.duration,
|
||||
),
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
type: PBadgeType.gray,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: SizedBox(
|
||||
height: 98,
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (item.type != 2) {
|
||||
SmartDialog.showToast('不支持播放该类型视频');
|
||||
return;
|
||||
}
|
||||
Get.back();
|
||||
widget.onChangeEpisode(item);
|
||||
},
|
||||
onLongPress: () => imageSaveDialog(
|
||||
title: item.title,
|
||||
cover: item.cover,
|
||||
aid: item.aid,
|
||||
bvid: item.bvid,
|
||||
),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 5,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Text(
|
||||
item.title!,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: isCurr ? FontWeight.bold : null,
|
||||
color: isCurr ? theme.colorScheme.primary : null,
|
||||
),
|
||||
NetworkImgLayer(
|
||||
src: item.cover,
|
||||
width: 140.8,
|
||||
height: 88,
|
||||
),
|
||||
if (item.type == 24 &&
|
||||
item.intro?.isNotEmpty == true) ...[
|
||||
const SizedBox(height: 3),
|
||||
if (item.badge?.isNotEmpty == true)
|
||||
PBadge(
|
||||
text: item.badge,
|
||||
right: 6.0,
|
||||
top: 6.0,
|
||||
type: switch (item.badge) {
|
||||
'充电专属' => PBadgeType.error,
|
||||
_ => PBadgeType.primary,
|
||||
},
|
||||
),
|
||||
PBadge(
|
||||
text: DurationUtils.formatDuration(
|
||||
item.duration,
|
||||
),
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
type: PBadgeType.gray,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.intro!,
|
||||
item.title!,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: isCurr ? FontWeight.bold : null,
|
||||
color: isCurr
|
||||
? theme.colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
),
|
||||
if (item.type == 24 &&
|
||||
item.intro?.isNotEmpty == true) ...[
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
item.intro!,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: theme.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
Text(
|
||||
item.upper!.name!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: theme.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
if (item.type == 2) ...[
|
||||
const SizedBox(height: 3),
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
StatWidget(
|
||||
type: StatType.play,
|
||||
value: item.cntInfo!.play,
|
||||
),
|
||||
StatWidget(
|
||||
type: StatType.danmaku,
|
||||
value: item.cntInfo!.danmaku,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
const Spacer(),
|
||||
Text(
|
||||
item.upper!.name!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: theme.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
if (item.type == 2) ...[
|
||||
const SizedBox(height: 3),
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
StatWidget(
|
||||
type: StatType.play,
|
||||
value: item.cntInfo!.play,
|
||||
),
|
||||
StatWidget(
|
||||
type: StatType.danmaku,
|
||||
value: item.cntInfo!.danmaku,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showDelBtn && !isCurr)
|
||||
Positioned(
|
||||
right: 12,
|
||||
bottom: -6,
|
||||
child: InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: () => showConfirmDialog(
|
||||
context: context,
|
||||
title: '确定移除该视频?',
|
||||
onConfirm: () => widget.onDelete!(item, index),
|
||||
),
|
||||
onLongPress: () => widget.onDelete!(item, index),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(9),
|
||||
child: Icon(
|
||||
Icons.clear,
|
||||
size: 18,
|
||||
color: theme.colorScheme.outline,
|
||||
if (showDelBtn && !isCurr)
|
||||
Positioned(
|
||||
right: 12,
|
||||
bottom: -6,
|
||||
child: InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: () => showConfirmDialog(
|
||||
context: context,
|
||||
title: '确定移除该视频?',
|
||||
onConfirm: () => widget.onDelete!(item, index),
|
||||
),
|
||||
onLongPress: () => widget.onDelete!(item, index),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(9),
|
||||
child: Icon(
|
||||
Icons.clear,
|
||||
size: 18,
|
||||
color: theme.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -36,14 +36,13 @@ class NoteListPage extends CommonSlidePage {
|
||||
State<NoteListPage> createState() => _NoteListPageState();
|
||||
}
|
||||
|
||||
class _NoteListPageState extends CommonSlidePageState<NoteListPage> {
|
||||
class _NoteListPageState extends State<NoteListPage>
|
||||
with SingleTickerProviderStateMixin, CommonSlideMixin {
|
||||
late final _controller = Get.put(
|
||||
NoteListPageCtr(oid: widget.oid, upperMid: widget.upperMid),
|
||||
tag: widget.heroTag,
|
||||
);
|
||||
|
||||
final _key = GlobalKey<ScaffoldState>();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
Get.delete<NoteListPageCtr>(tag: widget.heroTag);
|
||||
@@ -54,7 +53,6 @@ class _NoteListPageState extends CommonSlidePageState<NoteListPage> {
|
||||
Widget buildPage(ThemeData theme) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
key: _key,
|
||||
body: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
@@ -92,6 +90,14 @@ class _NoteListPageState extends CommonSlidePageState<NoteListPage> {
|
||||
);
|
||||
}
|
||||
|
||||
late Key _key;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_key = ValueKey(PrimaryScrollController.of(context).hashCode);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildList(ThemeData theme) {
|
||||
return refreshIndicator(
|
||||
@@ -101,7 +107,7 @@ class _NoteListPageState extends CommonSlidePageState<NoteListPage> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
controller: _controller.scrollController,
|
||||
key: _key,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
@@ -129,30 +135,32 @@ class _NoteListPageState extends CommonSlidePageState<NoteListPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
child: FilledButton.tonal(
|
||||
style: FilledButton.styleFrom(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
padding: EdgeInsets.zero,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
if (!Accounts.main.isLogin) {
|
||||
SmartDialog.showToast('账号未登录');
|
||||
return;
|
||||
}
|
||||
_key.currentState?.showBottomSheet(
|
||||
constraints: const BoxConstraints(),
|
||||
(context) => WebviewPage(
|
||||
oid: widget.oid,
|
||||
title: widget.title,
|
||||
url:
|
||||
'https://www.bilibili.com/h5/note-app?oid=${widget.oid}&pagefrom=ugcvideo&is_stein_gate=${widget.isStein ? 1 : 0}',
|
||||
child: Builder(
|
||||
builder: (context) => FilledButton.tonal(
|
||||
style: FilledButton.styleFrom(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
padding: EdgeInsets.zero,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('开始记笔记'),
|
||||
),
|
||||
onPressed: () {
|
||||
if (!Accounts.main.isLogin) {
|
||||
SmartDialog.showToast('账号未登录');
|
||||
return;
|
||||
}
|
||||
Scaffold.of(context).showBottomSheet(
|
||||
constraints: const BoxConstraints(),
|
||||
(context) => WebviewPage(
|
||||
oid: widget.oid,
|
||||
title: widget.title,
|
||||
url:
|
||||
'https://www.bilibili.com/h5/note-app?oid=${widget.oid}&pagefrom=ugcvideo&is_stein_gate=${widget.isStein ? 1 : 0}',
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('开始记笔记'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -169,15 +177,10 @@ class _NoteListPageState extends CommonSlidePageState<NoteListPage> {
|
||||
color: theme.colorScheme.outline.withValues(alpha: 0.1),
|
||||
);
|
||||
return switch (loadingState) {
|
||||
Loading() => SliverToBoxAdapter(
|
||||
child: IgnorePointer(
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) => const VideoReplySkeleton(),
|
||||
itemCount: 8,
|
||||
),
|
||||
),
|
||||
Loading() => SliverPrototypeExtentList.builder(
|
||||
prototypeItem: const VideoReplySkeleton(),
|
||||
itemBuilder: (_, _) => const VideoReplySkeleton(),
|
||||
itemCount: 8,
|
||||
),
|
||||
Success(:var response) =>
|
||||
response?.isNotEmpty == true
|
||||
|
||||
@@ -10,7 +10,7 @@ import 'package:PiliPlus/models/common/sponsor_block/action_type.dart';
|
||||
import 'package:PiliPlus/models/common/sponsor_block/post_segment_model.dart';
|
||||
import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart';
|
||||
import 'package:PiliPlus/models_new/sponsor_block/segment_item.dart';
|
||||
import 'package:PiliPlus/pages/common/slide/common_collapse_slide_page.dart';
|
||||
import 'package:PiliPlus/pages/common/slide/common_slide_page.dart';
|
||||
import 'package:PiliPlus/pages/video/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/post_panel/popup_menu_text.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/controller.dart';
|
||||
@@ -24,7 +24,7 @@ import 'package:flutter/services.dart' show FilteringTextInputFormatter;
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart' hide Response;
|
||||
|
||||
class PostPanel extends CommonCollapseSlidePage {
|
||||
class PostPanel extends CommonSlidePage {
|
||||
const PostPanel({
|
||||
super.key,
|
||||
super.enableSlide,
|
||||
@@ -179,7 +179,8 @@ class PostPanel extends CommonCollapseSlidePage {
|
||||
}
|
||||
}
|
||||
|
||||
class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
|
||||
class _PostPanelState extends State<PostPanel>
|
||||
with SingleTickerProviderStateMixin, CommonSlideMixin {
|
||||
late final VideoDetailController videoDetailController =
|
||||
widget.videoDetailController;
|
||||
late final PlPlayerController plPlayerController = widget.plPlayerController;
|
||||
@@ -191,14 +192,6 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
|
||||
double get currentPos =>
|
||||
plPlayerController.position.value.inMilliseconds / 1000;
|
||||
|
||||
final _controller = ScrollController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildPage(ThemeData theme) {
|
||||
return Scaffold(
|
||||
@@ -246,6 +239,14 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
|
||||
);
|
||||
}
|
||||
|
||||
late Key _key;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_key = ValueKey(PrimaryScrollController.of(context).hashCode);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildList(ThemeData theme) {
|
||||
if (list.isNullOrEmpty) {
|
||||
@@ -256,7 +257,7 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
ListView.builder(
|
||||
controller: _controller,
|
||||
key: _key,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.only(bottom: 88 + bottom),
|
||||
itemCount: list.length,
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:PiliPlus/common/widgets/video_card/video_card_h.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/models/model_hot_video_item.dart';
|
||||
import 'package:PiliPlus/pages/video/related/controller.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/grid.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -14,19 +15,14 @@ class RelatedVideoPanel extends StatefulWidget {
|
||||
State<RelatedVideoPanel> createState() => _RelatedVideoPanelState();
|
||||
}
|
||||
|
||||
class _RelatedVideoPanelState extends State<RelatedVideoPanel>
|
||||
with AutomaticKeepAliveClientMixin, GridMixin {
|
||||
late final RelatedController _relatedController = Get.put(
|
||||
RelatedController(),
|
||||
class _RelatedVideoPanelState extends State<RelatedVideoPanel> with GridMixin {
|
||||
late final RelatedController _relatedController = Get.putOrFind(
|
||||
RelatedController.new,
|
||||
tag: widget.heroTag,
|
||||
);
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.only(top: 7, bottom: 100),
|
||||
sliver: Obx(() => _buildBody(_relatedController.loadingState.value)),
|
||||
|
||||
@@ -2,13 +2,16 @@ import 'package:PiliPlus/common/skeleton/video_reply.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/common/widgets/scaffold/bottom_sheet.dart';
|
||||
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||
show ReplyInfo;
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/pages/video/reply/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart';
|
||||
import 'package:PiliPlus/pages/video/reply_reply/view.dart';
|
||||
import 'package:PiliPlus/utils/feed_back.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart' hide showBottomSheet;
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
@@ -17,20 +20,16 @@ class VideoReplyPanel extends StatefulWidget {
|
||||
super.key,
|
||||
this.replyLevel = 1,
|
||||
required this.heroTag,
|
||||
required this.replyReply,
|
||||
this.onViewImage,
|
||||
this.onDismissed,
|
||||
this.callback,
|
||||
required this.needController,
|
||||
required this.isNested,
|
||||
});
|
||||
|
||||
final int replyLevel;
|
||||
final String heroTag;
|
||||
final Function(ReplyInfo replyItem, int? rpid) replyReply;
|
||||
final VoidCallback? onViewImage;
|
||||
final ValueChanged<int>? onDismissed;
|
||||
final Function(List<String>, int)? callback;
|
||||
final bool needController;
|
||||
final bool isNested;
|
||||
|
||||
@override
|
||||
State<VideoReplyPanel> createState() => _VideoReplyPanelState();
|
||||
@@ -58,17 +57,17 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_videoReplyController.showFab();
|
||||
if (widget.needController != false) {
|
||||
_videoReplyController.scrollController.addListener(listener);
|
||||
} else {
|
||||
if (widget.isNested) {
|
||||
_videoReplyController.scrollController.removeListener(listener);
|
||||
} else {
|
||||
_videoReplyController.scrollController.addListener(listener);
|
||||
}
|
||||
bottom = MediaQuery.viewPaddingOf(context).bottom;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (widget.needController != false) {
|
||||
if (!widget.isNested) {
|
||||
_videoReplyController.scrollController.removeListener(listener);
|
||||
}
|
||||
super.dispose();
|
||||
@@ -100,14 +99,14 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
CustomScrollView(
|
||||
controller: widget.needController
|
||||
? _videoReplyController.scrollController
|
||||
: null,
|
||||
physics: widget.needController
|
||||
? const AlwaysScrollableScrollPhysics()
|
||||
: const AlwaysScrollableScrollPhysics(
|
||||
controller: widget.isNested
|
||||
? null
|
||||
: _videoReplyController.scrollController,
|
||||
physics: widget.isNested
|
||||
? const AlwaysScrollableScrollPhysics(
|
||||
parent: ClampingScrollPhysics(),
|
||||
),
|
||||
)
|
||||
: const AlwaysScrollableScrollPhysics(),
|
||||
key: const PageStorageKey<String>('评论'),
|
||||
slivers: <Widget>[
|
||||
SliverPersistentHeader(
|
||||
@@ -186,14 +185,17 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(ThemeData theme, LoadingState loadingState) {
|
||||
Widget _buildBody(
|
||||
ThemeData theme,
|
||||
LoadingState<List<ReplyInfo>?> loadingState,
|
||||
) {
|
||||
return switch (loadingState) {
|
||||
Loading() => SliverList.builder(
|
||||
itemBuilder: (context, index) => const VideoReplySkeleton(),
|
||||
itemCount: 5,
|
||||
),
|
||||
Success(:var response) =>
|
||||
response?.isNotEmpty == true
|
||||
response != null && response.isNotEmpty
|
||||
? SliverList.builder(
|
||||
itemBuilder: (context, index) {
|
||||
if (index == response.length) {
|
||||
@@ -215,7 +217,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
return ReplyItemGrpc(
|
||||
replyItem: response[index],
|
||||
replyLevel: widget.replyLevel,
|
||||
replyReply: widget.replyReply,
|
||||
replyReply: replyReply,
|
||||
onReply: (replyItem) => _videoReplyController.onReply(
|
||||
context,
|
||||
replyItem: replyItem,
|
||||
@@ -226,7 +228,6 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
getTag: () => heroTag,
|
||||
onViewImage: widget.onViewImage,
|
||||
onDismissed: widget.onDismissed,
|
||||
callback: widget.callback,
|
||||
onCheckReply: (item) => _videoReplyController
|
||||
.onCheckReply(item, isManual: true),
|
||||
onToggleTop: (item) => _videoReplyController.onToggleTop(
|
||||
@@ -250,4 +251,28 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// 展示二级回复
|
||||
void replyReply(ReplyInfo replyItem, int? id) {
|
||||
EasyThrottle.throttle('replyReply', const Duration(milliseconds: 500), () {
|
||||
int oid = replyItem.oid.toInt();
|
||||
int rpid = replyItem.id.toInt();
|
||||
showBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
constraints: const BoxConstraints(),
|
||||
builder: (context) => VideoReplyReplyPanel(
|
||||
id: id,
|
||||
oid: oid,
|
||||
rpid: rpid,
|
||||
firstFloor: replyItem,
|
||||
replyType: _videoReplyController.videoType.replyType,
|
||||
isVideoDetail: true,
|
||||
onViewImage: widget.onViewImage,
|
||||
onDismissed: widget.onDismissed,
|
||||
isNested: widget.isNested,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,9 +51,9 @@ class ReplyItemGrpc extends StatelessWidget {
|
||||
this.getTag,
|
||||
this.onViewImage,
|
||||
this.onDismissed,
|
||||
this.callback,
|
||||
this.onCheckReply,
|
||||
this.onToggleTop,
|
||||
this.jumpToDialogue,
|
||||
});
|
||||
final ReplyInfo replyItem;
|
||||
final int replyLevel;
|
||||
@@ -66,9 +66,9 @@ class ReplyItemGrpc extends StatelessWidget {
|
||||
final Function? getTag;
|
||||
final VoidCallback? onViewImage;
|
||||
final ValueChanged<int>? onDismissed;
|
||||
final Function(List<String>, int)? callback;
|
||||
final ValueChanged<ReplyInfo>? onCheckReply;
|
||||
final ValueChanged<ReplyInfo>? onToggleTop;
|
||||
final VoidCallback? jumpToDialogue;
|
||||
|
||||
static final _voteRegExp = RegExp(r"^\{vote:\d+?\}$");
|
||||
static final _timeRegExp = RegExp(r'^\b(?:\d+[::])?\d+[::]\d+\b$');
|
||||
@@ -312,7 +312,6 @@ class ReplyItemGrpc extends StatelessWidget {
|
||||
.toList(),
|
||||
onViewImage: onViewImage,
|
||||
onDismissed: onDismissed,
|
||||
callback: callback,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -416,6 +415,24 @@ class ReplyItemGrpc extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (replyLevel == 3 &&
|
||||
needDivider &&
|
||||
replyItem.parent != replyItem.root)
|
||||
SizedBox(
|
||||
height: 32,
|
||||
child: TextButton(
|
||||
onPressed: jumpToDialogue,
|
||||
style: style,
|
||||
child: Text(
|
||||
'跳转回复',
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.outline,
|
||||
fontSize: theme.textTheme.labelMedium!.fontSize,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
ZanButtonGrpc(replyItem: replyItem),
|
||||
|
||||
@@ -7,8 +7,11 @@ import 'package:PiliPlus/pages/video/reply_new/view.dart';
|
||||
import 'package:PiliPlus/utils/id_utils.dart';
|
||||
import 'package:PiliPlus/utils/request_utils.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_navigation/src/dialog/dialog_route.dart';
|
||||
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||
@@ -22,10 +25,8 @@ class VideoReplyReplyController extends ReplyController
|
||||
required this.rpid,
|
||||
required this.dialog,
|
||||
required this.replyType,
|
||||
required this.isDialogue,
|
||||
});
|
||||
final int? dialog;
|
||||
final bool isDialogue;
|
||||
int? id;
|
||||
// 视频aid 请求时使用的oid
|
||||
int oid;
|
||||
@@ -34,12 +35,18 @@ class VideoReplyReplyController extends ReplyController
|
||||
int replyType;
|
||||
|
||||
bool hasRoot = false;
|
||||
late final Rx<ReplyInfo?> firstFloor = Rx<ReplyInfo?>(null);
|
||||
final Rx<ReplyInfo?> firstFloor = Rx(null);
|
||||
|
||||
final index = RxnInt();
|
||||
|
||||
int? index;
|
||||
AnimationController? animController;
|
||||
final listController = ListController();
|
||||
|
||||
AnimationController? _controller;
|
||||
AnimationController get animController => _controller ??= AnimationController(
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
late final horizontalPreview = Pref.horizontalPreview;
|
||||
|
||||
@override
|
||||
@@ -54,7 +61,7 @@ class VideoReplyReplyController extends ReplyController
|
||||
|
||||
@override
|
||||
List<ReplyInfo>? getDataList(response) {
|
||||
return isDialogue ? response.replies : response.root.replies;
|
||||
return dialog != null ? response.replies : response.root.replies;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -73,29 +80,7 @@ class VideoReplyReplyController extends ReplyController
|
||||
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;
|
||||
animController = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
try {
|
||||
listController.jumpToItem(
|
||||
index: index,
|
||||
scrollController: scrollController,
|
||||
alignment: 0.25,
|
||||
);
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: 800),
|
||||
animController?.forward,
|
||||
);
|
||||
this.index = null;
|
||||
} catch (_) {}
|
||||
});
|
||||
}
|
||||
setIndexById(Int64(id!), data.root.replies);
|
||||
id = null;
|
||||
}
|
||||
}
|
||||
@@ -103,8 +88,41 @@ class VideoReplyReplyController extends ReplyController
|
||||
return false;
|
||||
}
|
||||
|
||||
bool setIndexById(Int64 id64, [List<ReplyInfo>? replies]) {
|
||||
final index = (replies ?? loadingState.value.data!).indexWhere(
|
||||
(item) => item.id == id64,
|
||||
);
|
||||
if (index != -1) {
|
||||
this.index.value = index;
|
||||
jumpToItem(index);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ExtendedNestedScrollController? nestedController;
|
||||
|
||||
void jumpToItem(int index) {
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
animController.forward(from: 0);
|
||||
try {
|
||||
// ignore: invalid_use_of_visible_for_testing_member
|
||||
final offset = listController.getOffsetToReveal(index, 0.25);
|
||||
if (offset.isFinite) {
|
||||
if (nestedController case final nestedController?) {
|
||||
nestedController.nestedPositions.last.localJumpTo(offset);
|
||||
} else {
|
||||
scrollController.jumpTo(offset);
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
if (kDebugMode) rethrow;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LoadingState> customGetData() => isDialogue
|
||||
Future<LoadingState> customGetData() => dialog != null
|
||||
? ReplyGrpc.dialogList(
|
||||
type: replyType,
|
||||
oid: oid,
|
||||
@@ -129,6 +147,14 @@ class VideoReplyReplyController extends ReplyController
|
||||
onReload();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onReload() {
|
||||
if (loadingState.value.isSuccess) {
|
||||
index.value = null;
|
||||
}
|
||||
return super.onReload();
|
||||
}
|
||||
|
||||
@override
|
||||
void onReply(
|
||||
BuildContext context, {
|
||||
@@ -201,8 +227,8 @@ class VideoReplyReplyController extends ReplyController
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
animController?.dispose();
|
||||
listController.dispose();
|
||||
_controller?.dispose();
|
||||
_controller = null;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,18 @@ import 'package:PiliPlus/common/skeleton/video_reply.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/common/widgets/scaffold/scaffold.dart';
|
||||
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||
show ReplyInfo, Mode;
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/pages/common/slide/common_slide_page.dart';
|
||||
import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart';
|
||||
import 'package:PiliPlus/pages/video/reply_reply/controller.dart';
|
||||
import 'package:PiliPlus/utils/context_ext.dart';
|
||||
import 'package:PiliPlus/utils/num_utils.dart';
|
||||
import 'package:PiliPlus/utils/page_utils.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
||||
import 'package:flutter/material.dart' hide Scaffold, ScaffoldState;
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart' hide ContextExtensionss;
|
||||
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||
|
||||
@@ -27,9 +28,9 @@ class VideoReplyReplyPanel extends CommonSlidePage {
|
||||
this.firstFloor,
|
||||
required this.isVideoDetail,
|
||||
required this.replyType,
|
||||
this.isDialogue = false,
|
||||
this.onViewImage,
|
||||
this.onDismissed,
|
||||
this.isNested = false,
|
||||
});
|
||||
final int? id;
|
||||
final int oid;
|
||||
@@ -38,28 +39,33 @@ class VideoReplyReplyPanel extends CommonSlidePage {
|
||||
final ReplyInfo? firstFloor;
|
||||
final bool isVideoDetail;
|
||||
final int replyType;
|
||||
final bool isDialogue;
|
||||
final VoidCallback? onViewImage;
|
||||
final ValueChanged<int>? onDismissed;
|
||||
final bool isNested;
|
||||
|
||||
@override
|
||||
State<VideoReplyReplyPanel> createState() => _VideoReplyReplyPanelState();
|
||||
}
|
||||
|
||||
class _VideoReplyReplyPanelState
|
||||
extends CommonSlidePageState<VideoReplyReplyPanel> {
|
||||
class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel>
|
||||
with SingleTickerProviderStateMixin, CommonSlideMixin {
|
||||
late VideoReplyReplyController _controller;
|
||||
late final _key = GlobalKey<ScaffoldState>();
|
||||
late final _tag = Utils.makeHeroTag(
|
||||
'${widget.rpid}${widget.dialog}${widget.isDialogue}',
|
||||
);
|
||||
|
||||
bool get _horizontalPreview =>
|
||||
_controller.horizontalPreview && context.isLandscape;
|
||||
Function(List<String> imgList, int index)? _imageCallback;
|
||||
|
||||
late final _tag = Utils.makeHeroTag('${widget.rpid}${widget.dialog}');
|
||||
Animation<Color?>? colorAnimation;
|
||||
|
||||
late final bool isDialogue = widget.dialog != null;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final controller = PrimaryScrollController.of(context);
|
||||
_controller
|
||||
..didChangeDependencies(context)
|
||||
..nestedController = controller is ExtendedNestedScrollController
|
||||
? controller
|
||||
: null;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -71,7 +77,6 @@ class _VideoReplyReplyPanelState
|
||||
rpid: widget.rpid,
|
||||
dialog: widget.dialog,
|
||||
replyType: widget.replyType,
|
||||
isDialogue: widget.isDialogue,
|
||||
),
|
||||
tag: _tag,
|
||||
);
|
||||
@@ -83,24 +88,10 @@ class _VideoReplyReplyPanelState
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_imageCallback = _horizontalPreview
|
||||
? (imgList, index) => PageUtils.onHorizontalPreview(
|
||||
_key,
|
||||
this,
|
||||
imgList,
|
||||
index,
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildPage(ThemeData theme) {
|
||||
Widget child() => enableSlide ? slideList(theme) : buildList(theme);
|
||||
return Scaffold(
|
||||
key: _key,
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: widget.isVideoDetail
|
||||
? Column(
|
||||
@@ -119,7 +110,7 @@ class _VideoReplyReplyPanelState
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(widget.isDialogue ? '对话列表' : '评论详情'),
|
||||
Text(isDialogue ? '对话列表' : '评论详情'),
|
||||
IconButton(
|
||||
tooltip: '关闭',
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
@@ -138,14 +129,23 @@ class _VideoReplyReplyPanelState
|
||||
ReplyInfo? get firstFloor =>
|
||||
widget.firstFloor ?? _controller.firstFloor.value;
|
||||
|
||||
ScrollController get scrollController =>
|
||||
_controller.nestedController ?? _controller.scrollController;
|
||||
|
||||
@override
|
||||
Widget buildList(ThemeData theme) {
|
||||
return refreshIndicator(
|
||||
onRefresh: _controller.onRefresh,
|
||||
child: CustomScrollView(
|
||||
controller: _controller.scrollController,
|
||||
key: ValueKey(scrollController.hashCode),
|
||||
controller: scrollController,
|
||||
physics: widget.isNested
|
||||
? const AlwaysScrollableScrollPhysics(
|
||||
parent: ClampingScrollPhysics(),
|
||||
)
|
||||
: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
if (!widget.isDialogue) ...[
|
||||
if (!isDialogue) ...[
|
||||
if (widget.firstFloor case final firstFloor?)
|
||||
_header(theme, firstFloor)
|
||||
else
|
||||
@@ -180,7 +180,6 @@ class _VideoReplyReplyPanelState
|
||||
upMid: _controller.upMid,
|
||||
onViewImage: widget.onViewImage,
|
||||
onDismissed: widget.onDismissed,
|
||||
callback: _imageCallback,
|
||||
onCheckReply: (item) =>
|
||||
_controller.onCheckReply(item, isManual: true),
|
||||
),
|
||||
@@ -252,18 +251,14 @@ class _VideoReplyReplyPanelState
|
||||
ThemeData theme,
|
||||
LoadingState<List<ReplyInfo>?> loadingState,
|
||||
) {
|
||||
final jumpIndex = _controller.index.value;
|
||||
return switch (loadingState) {
|
||||
Loading() => SliverToBoxAdapter(
|
||||
child: IgnorePointer(
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) => const VideoReplySkeleton(),
|
||||
itemCount: 8,
|
||||
),
|
||||
),
|
||||
Loading() => SliverPrototypeExtentList.builder(
|
||||
prototypeItem: const VideoReplySkeleton(),
|
||||
itemBuilder: (_, _) => const VideoReplySkeleton(),
|
||||
itemCount: 8,
|
||||
),
|
||||
Success(:var response) => SuperSliverList.builder(
|
||||
Success(:var response!) => SuperSliverList.builder(
|
||||
listController: _controller.listController,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == response.length) {
|
||||
@@ -284,19 +279,23 @@ class _VideoReplyReplyPanelState
|
||||
),
|
||||
);
|
||||
}
|
||||
final child = _replyItem(response[index], index);
|
||||
if (_controller.index == index) {
|
||||
colorAnimation ??= ColorTween(
|
||||
begin: theme.colorScheme.onInverseSurface,
|
||||
end: theme.colorScheme.surface,
|
||||
).animate(_controller.animController!);
|
||||
final child = _replyItem(context, response[index], index);
|
||||
if (jumpIndex == index) {
|
||||
return AnimatedBuilder(
|
||||
animation: colorAnimation!,
|
||||
builder: (context, _) {
|
||||
animation: colorAnimation ??=
|
||||
ColorTween(
|
||||
begin: theme.colorScheme.onInverseSurface,
|
||||
end: theme.colorScheme.surface,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller.animController,
|
||||
curve: const Interval(0.8, 1.0), // 前0.8s不变, 后0.2s开始动画
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
builder: (context, child) {
|
||||
return ColoredBox(
|
||||
color:
|
||||
colorAnimation!.value ??
|
||||
theme.colorScheme.onInverseSurface,
|
||||
color: colorAnimation!.value!,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
@@ -304,7 +303,7 @@ class _VideoReplyReplyPanelState
|
||||
}
|
||||
return child;
|
||||
},
|
||||
itemCount: response!.length + 1,
|
||||
itemCount: response.length + 1,
|
||||
),
|
||||
Error(:var errMsg) => HttpError(
|
||||
errMsg: errMsg,
|
||||
@@ -313,15 +312,15 @@ class _VideoReplyReplyPanelState
|
||||
};
|
||||
}
|
||||
|
||||
Widget _replyItem(ReplyInfo replyItem, int index) {
|
||||
Widget _replyItem(BuildContext context, ReplyInfo replyItem, int index) {
|
||||
return ReplyItemGrpc(
|
||||
replyItem: replyItem,
|
||||
replyLevel: widget.isDialogue ? 3 : 2,
|
||||
replyLevel: isDialogue ? 3 : 2,
|
||||
onReply: (replyItem) =>
|
||||
_controller.onReply(context, replyItem: replyItem, index: index),
|
||||
_controller.onReply(this.context, replyItem: replyItem, index: index),
|
||||
onDelete: (item, subIndex) => _controller.onRemove(index, item, null),
|
||||
upMid: _controller.upMid,
|
||||
showDialogue: () => _key.currentState?.showBottomSheet(
|
||||
showDialogue: () => Scaffold.of(context).showBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
constraints: const BoxConstraints(),
|
||||
(context) => VideoReplyReplyPanel(
|
||||
@@ -330,12 +329,16 @@ class _VideoReplyReplyPanelState
|
||||
dialog: replyItem.dialog.toInt(),
|
||||
replyType: widget.replyType,
|
||||
isVideoDetail: true,
|
||||
isDialogue: true,
|
||||
isNested: widget.isNested,
|
||||
),
|
||||
),
|
||||
jumpToDialogue: () {
|
||||
if (!_controller.setIndexById(replyItem.parent)) {
|
||||
SmartDialog.showToast('评论可能已被删除');
|
||||
}
|
||||
},
|
||||
onViewImage: widget.onViewImage,
|
||||
onDismissed: widget.onDismissed,
|
||||
callback: _imageCallback,
|
||||
onCheckReply: (item) => _controller.onCheckReply(item, isManual: true),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import 'dart:ui';
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/custom_icon.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart';
|
||||
import 'package:PiliPlus/common/widgets/scaffold/scaffold.dart';
|
||||
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
|
||||
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||
show ReplyInfo;
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/main.dart';
|
||||
import 'package:PiliPlus/models/common/episode_panel_type.dart';
|
||||
@@ -33,7 +33,6 @@ import 'package:PiliPlus/pages/video/member/view.dart';
|
||||
import 'package:PiliPlus/pages/video/related/view.dart';
|
||||
import 'package:PiliPlus/pages/video/reply/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/reply/view.dart';
|
||||
import 'package:PiliPlus/pages/video/reply_reply/view.dart';
|
||||
import 'package:PiliPlus/pages/video/view_point/view.dart';
|
||||
import 'package:PiliPlus/pages/video/widgets/focus.dart';
|
||||
import 'package:PiliPlus/pages/video/widgets/header_control.dart';
|
||||
@@ -54,11 +53,10 @@ import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/storage_key.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:auto_orientation/auto_orientation.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
||||
import 'package:floating/floating.dart';
|
||||
import 'package:flutter/foundation.dart' show kDebugMode;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart' hide Scaffold;
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart' show SystemUiOverlayStyle;
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
@@ -117,15 +115,10 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
((videoDetail.pages?.length ?? 0) > 1));
|
||||
}
|
||||
|
||||
bool get _horizontalPreview =>
|
||||
!isPortrait && videoDetailController.plPlayerController.horizontalPreview;
|
||||
|
||||
final GlobalKey relatedVideoPanelKey = GlobalKey();
|
||||
final GlobalKey videoPlayerKey = GlobalKey();
|
||||
final GlobalKey playerKey = GlobalKey();
|
||||
final GlobalKey videoReplyPanelKey = GlobalKey();
|
||||
late final GlobalKey ugcPanelKey = GlobalKey();
|
||||
late final GlobalKey pgcPanelKey = GlobalKey();
|
||||
final videoPlayerKey = GlobalKey();
|
||||
final videoReplyPanelKey = GlobalKey();
|
||||
final videoRelatedKey = GlobalKey();
|
||||
final videoIntroKey = GlobalKey();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -470,8 +463,6 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
plPlayerController?.addPositionListener(positionListener);
|
||||
}
|
||||
|
||||
Function(List<String> imgList, int index)? _imageCallback;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
@@ -498,14 +489,6 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
themeData = videoDetailController.plPlayerController.darkVideoPage
|
||||
? MyApp.darkThemeData ?? Theme.of(context)
|
||||
: Theme.of(context);
|
||||
_imageCallback = _horizontalPreview
|
||||
? (imgList, index) => PageUtils.onHorizontalPreview(
|
||||
videoDetailController.childKey,
|
||||
this,
|
||||
imgList,
|
||||
index,
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
void animListener() {
|
||||
@@ -580,7 +563,6 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
final isFullScreen = this.isFullScreen;
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
key: videoDetailController.scaffoldKey,
|
||||
appBar: isFullScreen
|
||||
? null
|
||||
: PreferredSize(
|
||||
@@ -631,9 +613,6 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
),
|
||||
body: ExtendedNestedScrollView(
|
||||
key: videoDetailController.scrollKey,
|
||||
physics: const NeverScrollableScrollPhysics(
|
||||
parent: ClampingScrollPhysics(),
|
||||
),
|
||||
controller: videoDetailController.scrollCtr,
|
||||
onlyOneScrollInBody: true,
|
||||
pinnedHeaderSliverHeightBuilder: () {
|
||||
@@ -972,7 +951,7 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
children: [
|
||||
videoIntro(isHorizontal: false, needCtr: false),
|
||||
if (videoDetailController.showReply)
|
||||
videoReplyPanel(false),
|
||||
videoReplyPanel(isNested: true),
|
||||
if (_shouldShowSeasonPanel) seasonPanel,
|
||||
],
|
||||
),
|
||||
@@ -992,7 +971,6 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
final padding = MediaQuery.viewPaddingOf(context);
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
key: videoDetailController.scaffoldKey,
|
||||
appBar: isFullScreen
|
||||
? null
|
||||
: AppBar(backgroundColor: Colors.black, toolbarHeight: 0),
|
||||
@@ -1137,14 +1115,16 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
videoDetailController
|
||||
.plPlayerController
|
||||
.showRelatedVideo)
|
||||
CustomScrollView(
|
||||
controller: introScrollController,
|
||||
slivers: [
|
||||
RelatedVideoPanel(
|
||||
key: relatedVideoPanelKey,
|
||||
heroTag: heroTag,
|
||||
),
|
||||
],
|
||||
KeepAliveWrapper(
|
||||
builder: (context) => CustomScrollView(
|
||||
controller: introScrollController,
|
||||
slivers: [
|
||||
RelatedVideoPanel(
|
||||
key: videoRelatedKey,
|
||||
heroTag: heroTag,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (videoDetailController.showReply) videoReplyPanel(),
|
||||
if (_shouldShowSeasonPanel) seasonPanel,
|
||||
@@ -1165,7 +1145,6 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
final padding = MediaQuery.viewPaddingOf(context);
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
key: videoDetailController.scaffoldKey,
|
||||
appBar: isFullScreen
|
||||
? null
|
||||
: AppBar(backgroundColor: Colors.black, toolbarHeight: 0),
|
||||
@@ -1449,7 +1428,6 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
plPlayerController?.videoController == null
|
||||
? const SizedBox.shrink()
|
||||
: PLVideoPlayer(
|
||||
key: playerKey,
|
||||
maxWidth: width,
|
||||
maxHeight: height,
|
||||
plPlayerController: plPlayerController!,
|
||||
@@ -1833,64 +1811,69 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
bool needCtr = true,
|
||||
}) {
|
||||
final bottom = MediaQuery.viewPaddingOf(context).bottom;
|
||||
Widget introPanel() => CustomScrollView(
|
||||
key: const PageStorageKey<String>('简介'),
|
||||
controller: needCtr ? introScrollController : null,
|
||||
physics: !needCtr
|
||||
? const AlwaysScrollableScrollPhysics(parent: ClampingScrollPhysics())
|
||||
: null,
|
||||
slivers: [
|
||||
if (videoDetailController.isUgc) ...[
|
||||
UgcIntroPanel(
|
||||
key: ugcPanelKey,
|
||||
heroTag: heroTag,
|
||||
showAiBottomSheet: showAiBottomSheet,
|
||||
showEpisodes: showEpisodes,
|
||||
onShowMemberPage: onShowMemberPage,
|
||||
isPortrait: isPortrait,
|
||||
isHorizontal: isHorizontal ?? width! > height! * 1.25,
|
||||
),
|
||||
if (needRelated &&
|
||||
videoDetailController.plPlayerController.showRelatedVideo) ...[
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: StyleString.safeSpace),
|
||||
child: Divider(
|
||||
height: 1,
|
||||
indent: 12,
|
||||
endIndent: 12,
|
||||
color: themeData.colorScheme.outline.withValues(alpha: 0.08),
|
||||
Widget introPanel() => KeepAliveWrapper(
|
||||
builder: (context) => CustomScrollView(
|
||||
controller: needCtr ? introScrollController : null,
|
||||
physics: !needCtr
|
||||
? const AlwaysScrollableScrollPhysics(
|
||||
parent: ClampingScrollPhysics(),
|
||||
)
|
||||
: null,
|
||||
slivers: [
|
||||
if (videoDetailController.isUgc) ...[
|
||||
UgcIntroPanel(
|
||||
key: videoIntroKey,
|
||||
heroTag: heroTag,
|
||||
showAiBottomSheet: showAiBottomSheet,
|
||||
showEpisodes: showEpisodes,
|
||||
onShowMemberPage: onShowMemberPage,
|
||||
isPortrait: isPortrait,
|
||||
isHorizontal: isHorizontal ?? width! > height! * 1.25,
|
||||
),
|
||||
if (needRelated &&
|
||||
videoDetailController.plPlayerController.showRelatedVideo) ...[
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: StyleString.safeSpace),
|
||||
child: Divider(
|
||||
height: 1,
|
||||
indent: 12,
|
||||
endIndent: 12,
|
||||
color: themeData.colorScheme.outline.withValues(
|
||||
alpha: 0.08,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
RelatedVideoPanel(key: videoRelatedKey, heroTag: heroTag),
|
||||
],
|
||||
] else
|
||||
PgcIntroPage(
|
||||
key: videoIntroKey,
|
||||
heroTag: heroTag,
|
||||
cid: videoDetailController.cid.value,
|
||||
showEpisodes: showEpisodes,
|
||||
showIntroDetail: showIntroDetail,
|
||||
maxWidth: width ?? maxWidth,
|
||||
isLandscape: !isPortrait,
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height:
|
||||
(videoDetailController.isPlayAll && !isPortrait
|
||||
? 80
|
||||
: StyleString.safeSpace) +
|
||||
bottom,
|
||||
),
|
||||
RelatedVideoPanel(key: relatedVideoPanelKey, heroTag: heroTag),
|
||||
],
|
||||
] else
|
||||
PgcIntroPage(
|
||||
key: pgcPanelKey,
|
||||
heroTag: heroTag,
|
||||
cid: videoDetailController.cid.value,
|
||||
showEpisodes: showEpisodes,
|
||||
showIntroDetail: showIntroDetail,
|
||||
maxWidth: width ?? maxWidth,
|
||||
isLandscape: !isPortrait,
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height:
|
||||
(videoDetailController.isPlayAll && !isPortrait
|
||||
? 80
|
||||
: StyleString.safeSpace) +
|
||||
bottom,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
introPanel(),
|
||||
if (videoDetailController.isPlayAll)
|
||||
if (videoDetailController.isPlayAll) {
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
introPanel(),
|
||||
Positioned(
|
||||
left: 12,
|
||||
right: 12,
|
||||
@@ -1928,29 +1911,74 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (Platform.isAndroid)
|
||||
const SizedBox.shrink(),
|
||||
],
|
||||
);
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return introPanel();
|
||||
}
|
||||
|
||||
Widget get seasonPanel {
|
||||
final videoDetail = ugcIntroController.videoDetail.value;
|
||||
return Column(
|
||||
children: [
|
||||
if ((videoDetail.pages?.length ?? 0) > 1)
|
||||
if (videoDetail.ugcSeason != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
child: PagesPanel(
|
||||
heroTag: heroTag,
|
||||
ugcIntroController: ugcIntroController,
|
||||
bvid: ugcIntroController.bvid,
|
||||
showEpisodes: showEpisodes,
|
||||
return KeepAliveWrapper(
|
||||
builder: (context) => Column(
|
||||
children: [
|
||||
if ((videoDetail.pages?.length ?? 0) > 1)
|
||||
if (videoDetail.ugcSeason != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
child: PagesPanel(
|
||||
heroTag: heroTag,
|
||||
ugcIntroController: ugcIntroController,
|
||||
bvid: ugcIntroController.bvid,
|
||||
showEpisodes: showEpisodes,
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() => EpisodePanel(
|
||||
heroTag: heroTag,
|
||||
enableSlide: false,
|
||||
ugcIntroController: videoDetailController.isUgc
|
||||
? ugcIntroController
|
||||
: null,
|
||||
type: EpisodeType.part,
|
||||
list: [videoDetail.pages!],
|
||||
cover: videoDetailController.cover.value,
|
||||
bvid: videoDetailController.bvid,
|
||||
aid: videoDetailController.aid,
|
||||
cid: videoDetailController.cid.value,
|
||||
isReversed: videoDetail.isPageReversed,
|
||||
onChangeEpisode: videoDetailController.isUgc
|
||||
? ugcIntroController.onChangeEpisode
|
||||
: pgcIntroController.onChangeEpisode,
|
||||
showTitle: false,
|
||||
isSupportReverse: videoDetailController.isUgc,
|
||||
onReverse: () => onReversePlay(isSeason: false),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
if (videoDetail.ugcSeason != null) ...[
|
||||
if ((videoDetail.pages?.length ?? 0) > 1) ...[
|
||||
const SizedBox(height: 8),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: themeData.colorScheme.outline.withValues(alpha: 0.1),
|
||||
),
|
||||
],
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Obx(
|
||||
() => SeasonPanel(
|
||||
key: ValueKey(introController.videoDetail.value),
|
||||
heroTag: heroTag,
|
||||
canTap: false,
|
||||
showEpisodes: showEpisodes,
|
||||
ugcIntroController: ugcIntroController,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() => EpisodePanel(
|
||||
@@ -1959,110 +1987,43 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
ugcIntroController: videoDetailController.isUgc
|
||||
? ugcIntroController
|
||||
: null,
|
||||
type: EpisodeType.part,
|
||||
list: [videoDetail.pages!],
|
||||
type: EpisodeType.season,
|
||||
initialTabIndex: videoDetailController.seasonIndex.value,
|
||||
cover: videoDetailController.cover.value,
|
||||
seasonId: videoDetail.ugcSeason!.id,
|
||||
list: videoDetail.ugcSeason!.sections!,
|
||||
bvid: videoDetailController.bvid,
|
||||
aid: videoDetailController.aid,
|
||||
cid: videoDetailController.cid.value,
|
||||
isReversed: videoDetail.isPageReversed,
|
||||
cid: videoDetailController.seasonCid ?? 0,
|
||||
isReversed: ugcIntroController
|
||||
.videoDetail
|
||||
.value
|
||||
.ugcSeason!
|
||||
.sections![videoDetailController.seasonIndex.value]
|
||||
.isReversed,
|
||||
onChangeEpisode: videoDetailController.isUgc
|
||||
? ugcIntroController.onChangeEpisode
|
||||
: pgcIntroController.onChangeEpisode,
|
||||
showTitle: false,
|
||||
isSupportReverse: videoDetailController.isUgc,
|
||||
onReverse: () => onReversePlay(isSeason: false),
|
||||
onReverse: () => onReversePlay(isSeason: true),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (videoDetail.ugcSeason != null) ...[
|
||||
if ((videoDetail.pages?.length ?? 0) > 1) ...[
|
||||
const SizedBox(height: 8),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: themeData.colorScheme.outline.withValues(alpha: 0.1),
|
||||
),
|
||||
],
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Obx(
|
||||
() => SeasonPanel(
|
||||
key: ValueKey(introController.videoDetail.value),
|
||||
heroTag: heroTag,
|
||||
canTap: false,
|
||||
showEpisodes: showEpisodes,
|
||||
ugcIntroController: ugcIntroController,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() => EpisodePanel(
|
||||
heroTag: heroTag,
|
||||
enableSlide: false,
|
||||
ugcIntroController: videoDetailController.isUgc
|
||||
? ugcIntroController
|
||||
: null,
|
||||
type: EpisodeType.season,
|
||||
initialTabIndex: videoDetailController.seasonIndex.value,
|
||||
cover: videoDetailController.cover.value,
|
||||
seasonId: videoDetail.ugcSeason!.id,
|
||||
list: videoDetail.ugcSeason!.sections!,
|
||||
bvid: videoDetailController.bvid,
|
||||
aid: videoDetailController.aid,
|
||||
cid: videoDetailController.seasonCid ?? 0,
|
||||
isReversed: ugcIntroController
|
||||
.videoDetail
|
||||
.value
|
||||
.ugcSeason!
|
||||
.sections![videoDetailController.seasonIndex.value]
|
||||
.isReversed,
|
||||
onChangeEpisode: videoDetailController.isUgc
|
||||
? ugcIntroController.onChangeEpisode
|
||||
: pgcIntroController.onChangeEpisode,
|
||||
showTitle: false,
|
||||
isSupportReverse: videoDetailController.isUgc,
|
||||
onReverse: () => onReversePlay(isSeason: true),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget videoReplyPanel([bool needCtr = true]) => VideoReplyPanel(
|
||||
Widget videoReplyPanel({bool isNested = false}) => VideoReplyPanel(
|
||||
key: videoReplyPanelKey,
|
||||
needController: needCtr,
|
||||
isNested: isNested,
|
||||
heroTag: heroTag,
|
||||
replyReply: replyReply,
|
||||
onViewImage: videoDetailController.onViewImage,
|
||||
onDismissed: videoDetailController.onDismissed,
|
||||
callback: _imageCallback,
|
||||
);
|
||||
|
||||
// 展示二级回复
|
||||
void replyReply(ReplyInfo replyItem, int? id) {
|
||||
EasyThrottle.throttle('replyReply', const Duration(milliseconds: 500), () {
|
||||
int oid = replyItem.oid.toInt();
|
||||
int rpid = replyItem.id.toInt();
|
||||
videoDetailController.childKey.currentState?.showBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
constraints: const BoxConstraints(),
|
||||
(context) => VideoReplyReplyPanel(
|
||||
id: id,
|
||||
oid: oid,
|
||||
rpid: rpid,
|
||||
firstFloor: replyItem,
|
||||
replyType: _videoReplyController.videoType.replyType,
|
||||
isVideoDetail: true,
|
||||
onViewImage: videoDetailController.onViewImage,
|
||||
onDismissed: videoDetailController.onDismissed,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ai总结
|
||||
void showAiBottomSheet() {
|
||||
videoDetailController.childKey.currentState?.showBottomSheet(
|
||||
|
||||
@@ -2,14 +2,14 @@ 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/common/widgets/progress_bar/segment_progress_bar.dart';
|
||||
import 'package:PiliPlus/pages/common/slide/common_collapse_slide_page.dart';
|
||||
import 'package:PiliPlus/pages/common/slide/common_slide_page.dart';
|
||||
import 'package:PiliPlus/pages/video/controller.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/controller.dart';
|
||||
import 'package:PiliPlus/utils/duration_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ViewPointsPage extends CommonCollapseSlidePage {
|
||||
class ViewPointsPage extends CommonSlidePage {
|
||||
const ViewPointsPage({
|
||||
super.key,
|
||||
super.enableSlide,
|
||||
@@ -24,22 +24,14 @@ class ViewPointsPage extends CommonCollapseSlidePage {
|
||||
State<ViewPointsPage> createState() => _ViewPointsPageState();
|
||||
}
|
||||
|
||||
class _ViewPointsPageState
|
||||
extends CommonCollapseSlidePageState<ViewPointsPage> {
|
||||
class _ViewPointsPageState extends State<ViewPointsPage>
|
||||
with SingleTickerProviderStateMixin, CommonSlideMixin {
|
||||
VideoDetailController get videoDetailController =>
|
||||
widget.videoDetailController;
|
||||
PlPlayerController? get plPlayerController => widget.plPlayerController;
|
||||
|
||||
int currentIndex = -1;
|
||||
|
||||
final _controller = ScrollController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildPage(ThemeData theme) {
|
||||
return Scaffold(
|
||||
@@ -93,10 +85,18 @@ class _ViewPointsPageState
|
||||
);
|
||||
}
|
||||
|
||||
late Key _key;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_key = ValueKey(PrimaryScrollController.of(context).hashCode);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildList(ThemeData theme) {
|
||||
return ListView.builder(
|
||||
controller: _controller,
|
||||
key: _key,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.only(
|
||||
top: 7,
|
||||
|
||||
@@ -86,7 +86,7 @@ class HeaderControlState extends TripleState<HeaderControl> {
|
||||
|
||||
bool get isFullScreen => plPlayerController.isFullScreen.value;
|
||||
Box setting = GStorage.setting;
|
||||
late final provider = ContextSingleTicker(context);
|
||||
late final provider = ContextSingleTicker(context, autoStart: false);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
||||
Reference in New Issue
Block a user