mod: more slide dismiss pages

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-03-04 20:35:01 +08:00
parent ef644d2837
commit 56c5ad360a
17 changed files with 1520 additions and 1519 deletions

View File

@@ -7,6 +7,7 @@ import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/http/video.dart'; import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/models/bangumi/info.dart' as bangumi; import 'package:PiliPlus/models/bangumi/info.dart' as bangumi;
import 'package:PiliPlus/models/video_detail_res.dart' as video; import 'package:PiliPlus/models/video_detail_res.dart' as video;
import 'package:PiliPlus/pages/common/common_slide_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
@@ -16,7 +17,7 @@ import '../../utils/storage.dart';
import '../../utils/utils.dart'; import '../../utils/utils.dart';
import 'package:PiliPlus/common/widgets/spring_physics.dart'; import 'package:PiliPlus/common/widgets/spring_physics.dart';
class ListSheetContent extends StatefulWidget { class ListSheetContent extends CommonSlidePage {
const ListSheetContent({ const ListSheetContent({
super.key, super.key,
this.index, // tab index this.index, // tab index
@@ -31,6 +32,7 @@ class ListSheetContent extends StatefulWidget {
this.showTitle, this.showTitle,
this.isSupportReverse, this.isSupportReverse,
this.isReversed, this.isReversed,
super.enableSlide,
}); });
final dynamic index; final dynamic index;
@@ -50,7 +52,7 @@ class ListSheetContent extends StatefulWidget {
State<ListSheetContent> createState() => _ListSheetContentState(); State<ListSheetContent> createState() => _ListSheetContentState();
} }
class _ListSheetContentState extends State<ListSheetContent> class _ListSheetContentState extends CommonSlidePageState<ListSheetContent>
with TickerProviderStateMixin { with TickerProviderStateMixin {
late List<ItemScrollController> itemScrollController = []; late List<ItemScrollController> itemScrollController = [];
late int currentIndex = _currentIndex; late int currentIndex = _currentIndex;
@@ -140,7 +142,7 @@ class _ListSheetContentState extends State<ListSheetContent>
}(); }();
} }
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (GStorage.collapsibleVideoPage) { if (enableSlide && GStorage.collapsibleVideoPage) {
if (mounted) { if (mounted) {
setState(() { setState(() {
_isInit = false; _isInit = false;
@@ -293,176 +295,195 @@ class _ListSheetContentState extends State<ListSheetContent>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (GStorage.collapsibleVideoPage && _isInit) { if (enableSlide && GStorage.collapsibleVideoPage && _isInit) {
return CustomScrollView( return CustomScrollView(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
); );
} }
return Column( return enableSlide
children: [ ? Padding(
Container( padding: EdgeInsets.only(top: padding),
height: 45, child: buildPage,
padding: EdgeInsets.symmetric( )
horizontal: widget.showTitle != false ? 14 : 6), : buildPage;
child: Row( }
children: [
if (widget.showTitle != false) @override
Text( Widget get buildPage => Material(
'合集(${_isList ? widget.season.epCount : episodes?.length ?? ''})', color: widget.showTitle == false
style: Theme.of(context).textTheme.titleMedium, ? Colors.transparent
), : Theme.of(context).colorScheme.surface,
StreamBuilder( child: Column(
stream: _favStream?.stream, children: [
builder: (context, snapshot) => snapshot.hasData Container(
? mediumButton( height: 45,
tooltip: _seasonFav == 1 ? '取消订阅' : '订阅', padding: EdgeInsets.symmetric(
icon: _seasonFav == 1 horizontal: widget.showTitle != false ? 14 : 6),
? Icons.notifications_off_outlined child: Row(
: Icons.notifications_active_outlined, children: [
onPressed: () async { if (widget.showTitle != false)
dynamic result = await VideoHttp.seasonFav( Text(
isFav: _seasonFav == 1, '合集(${_isList ? widget.season.epCount : episodes?.length ?? ''})',
seasonId: widget.season.id, style: Theme.of(context).textTheme.titleMedium,
); ),
if (result['status']) { StreamBuilder(
SmartDialog.showToast( stream: _favStream?.stream,
'${_seasonFav == 1 ? '取消' : ''}订阅成功'); builder: (context, snapshot) => snapshot.hasData
_seasonFav = _seasonFav == 1 ? 0 : 1; ? mediumButton(
_favStream?.add(_seasonFav); tooltip: _seasonFav == 1 ? '取消订阅' : '订阅',
} else { icon: _seasonFav == 1
SmartDialog.showToast(result['msg']); ? Icons.notifications_off_outlined
} : Icons.notifications_active_outlined,
onPressed: () async {
dynamic result = await VideoHttp.seasonFav(
isFav: _seasonFav == 1,
seasonId: widget.season.id,
);
if (result['status']) {
SmartDialog.showToast(
'${_seasonFav == 1 ? '取消' : ''}订阅成功');
_seasonFav = _seasonFav == 1 ? 0 : 1;
_favStream?.add(_seasonFav);
} else {
SmartDialog.showToast(result['msg']);
}
},
)
: const SizedBox.shrink(),
),
mediumButton(
tooltip: '跳至顶部',
icon: Icons.vertical_align_top,
onPressed: () {
try {
itemScrollController[_ctr?.index ?? 0].scrollTo(
index: !reverse[_ctr?.index ?? 0]
? 0
: _isList
? widget.season.sections[_ctr?.index].episodes
.length -
1
: episodes.length - 1,
duration: const Duration(milliseconds: 200),
);
} catch (_) {}
},
),
mediumButton(
tooltip: '跳至底部',
icon: Icons.vertical_align_bottom,
onPressed: () {
try {
itemScrollController[_ctr?.index ?? 0].scrollTo(
index: !reverse[_ctr?.index ?? 0]
? _isList
? widget.season.sections[_ctr?.index].episodes
.length -
1
: episodes.length - 1
: 0,
duration: const Duration(milliseconds: 200),
);
} catch (_) {}
},
),
mediumButton(
tooltip: '跳至当前',
icon: Icons.my_location,
onPressed: () async {
if (_ctr != null && _ctr?.index != (_index)) {
_ctr?.animateTo(_index);
await Future.delayed(const Duration(milliseconds: 225));
}
try {
itemScrollController[_ctr?.index ?? 0].scrollTo(
index: currentIndex,
duration: const Duration(milliseconds: 200),
);
} catch (_) {}
},
),
if (widget.isSupportReverse == true)
if (!_isList)
_reverseButton
else
StreamBuilder(
stream: _indexStream?.stream,
initialData: _index,
builder: (context, snapshot) {
return snapshot.data == _index
? _reverseButton
: const SizedBox.shrink();
}, },
) ),
: const SizedBox.shrink(), const Spacer(),
),
mediumButton(
tooltip: '跳至顶部',
icon: Icons.vertical_align_top,
onPressed: () {
try {
itemScrollController[_ctr?.index ?? 0].scrollTo(
index: !reverse[_ctr?.index ?? 0]
? 0
: _isList
? widget.season.sections[_ctr?.index].episodes
.length -
1
: episodes.length - 1,
duration: const Duration(milliseconds: 200),
);
} catch (_) {}
},
),
mediumButton(
tooltip: '跳至底部',
icon: Icons.vertical_align_bottom,
onPressed: () {
try {
itemScrollController[_ctr?.index ?? 0].scrollTo(
index: !reverse[_ctr?.index ?? 0]
? _isList
? widget.season.sections[_ctr?.index].episodes
.length -
1
: episodes.length - 1
: 0,
duration: const Duration(milliseconds: 200),
);
} catch (_) {}
},
),
mediumButton(
tooltip: '跳至当前',
icon: Icons.my_location,
onPressed: () async {
if (_ctr != null && _ctr?.index != (_index)) {
_ctr?.animateTo(_index);
await Future.delayed(const Duration(milliseconds: 225));
}
try {
itemScrollController[_ctr?.index ?? 0].scrollTo(
index: currentIndex,
duration: const Duration(milliseconds: 200),
);
} catch (_) {}
},
),
if (widget.isSupportReverse == true)
if (!_isList)
_reverseButton
else
StreamBuilder( StreamBuilder(
stream: _indexStream?.stream, stream: _indexStream?.stream,
initialData: _index, initialData: _index,
builder: (context, snapshot) { builder: (context, snapshot) => mediumButton(
return snapshot.data == _index tooltip: reverse[snapshot.data] ? '顺序' : '倒序',
? _reverseButton icon: !reverse[snapshot.data]
: const SizedBox.shrink(); ? MdiIcons.sortNumericAscending
}, : MdiIcons.sortNumericDescending,
), onPressed: () {
const Spacer(), setState(() {
StreamBuilder( reverse[_ctr?.index ?? 0] =
stream: _indexStream?.stream, !reverse[_ctr?.index ?? 0];
initialData: _index, });
builder: (context, snapshot) => mediumButton( },
tooltip: reverse[snapshot.data] ? '顺序' : '倒序',
icon: !reverse[snapshot.data]
? MdiIcons.sortNumericAscending
: MdiIcons.sortNumericDescending,
onPressed: () {
setState(() {
reverse[_ctr?.index ?? 0] = !reverse[_ctr?.index ?? 0];
});
},
),
),
if (widget.onClose != null)
mediumButton(
tooltip: '关闭',
icon: Icons.close,
onPressed: widget.onClose,
),
],
),
),
Divider(
height: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
if (_isList)
TabBar(
controller: _ctr,
padding: const EdgeInsets.only(right: 60),
isScrollable: true,
tabs: (widget.season.sections as List)
.map((item) => Tab(text: item.title))
.toList(),
dividerHeight: 1,
dividerColor: Theme.of(context).dividerColor.withOpacity(0.1),
),
Expanded(
child: _isList
? Material(
color: Colors.transparent,
child: tabBarView(
controller: _ctr,
children: List.generate(
widget.season.sections.length,
(index) => _buildBody(
index, widget.season.sections[index].episodes),
), ),
), ),
) if (widget.onClose != null)
: Material( mediumButton(
color: Colors.transparent, tooltip: '关闭',
child: _buildBody(null, episodes), icon: Icons.close,
), onPressed: widget.onClose,
),
],
),
),
Divider(
height: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
if (_isList)
TabBar(
controller: _ctr,
padding: const EdgeInsets.only(right: 60),
isScrollable: true,
tabs: (widget.season.sections as List)
.map((item) => Tab(text: item.title))
.toList(),
dividerHeight: 1,
dividerColor: Theme.of(context).dividerColor.withOpacity(0.1),
),
Expanded(
child: _isList
? Material(
color: Colors.transparent,
child: tabBarView(
controller: _ctr,
children: List.generate(
widget.season.sections.length,
(index) => _buildBody(
index, widget.season.sections[index].episodes),
),
),
)
: enableSlide
? slideList()
: buildList,
),
],
), ),
], );
);
} @override
Widget get buildList => Material(
color: Colors.transparent,
child: _buildBody(null, episodes),
);
Widget get _reverseButton => mediumButton( Widget get _reverseButton => mediumButton(
tooltip: widget.isReversed == true ? '正序播放' : '倒序播放', tooltip: widget.isReversed == true ? '正序播放' : '倒序播放',

View File

@@ -1,5 +1,7 @@
import 'dart:ui'; import 'dart:ui';
import 'package:PiliPlus/models/common/sponsor_block/action_type.dart';
enum SegmentType { enum SegmentType {
sponsor, sponsor,
selfpromo, selfpromo,
@@ -13,6 +15,74 @@ enum SegmentType {
exclusive_access exclusive_access
} }
// List<SegmentType> _actionType2SegmentType(ActionType actionType) {
// return switch (actionType) {
// ActionType.skip => [
// SegmentType.sponsor,
// SegmentType.selfpromo,
// SegmentType.interaction,
// SegmentType.intro,
// SegmentType.outro,
// SegmentType.preview,
// SegmentType.filler,
// ],
// ActionType.mute => [
// SegmentType.sponsor,
// SegmentType.selfpromo,
// SegmentType.interaction,
// SegmentType.intro,
// SegmentType.outro,
// SegmentType.preview,
// SegmentType.music_offtopic,
// SegmentType.filler,
// ],
// ActionType.full => [
// SegmentType.sponsor,
// SegmentType.selfpromo,
// SegmentType.exclusive_access,
// ],
// ActionType.poi => [
// SegmentType.poi_highlight,
// ],
// };
// }
List<ActionType> segmentType2ActionType(SegmentType segmentType) {
return switch (segmentType) {
SegmentType.sponsor => [ActionType.skip, ActionType.mute, ActionType.full],
SegmentType.selfpromo => [
ActionType.skip,
ActionType.mute,
ActionType.full
],
SegmentType.interaction => [
ActionType.skip,
ActionType.mute,
],
SegmentType.intro => [
ActionType.skip,
ActionType.mute,
],
SegmentType.outro => [
ActionType.skip,
ActionType.mute,
],
SegmentType.preview => [
ActionType.skip,
ActionType.mute,
],
SegmentType.music_offtopic => [
ActionType.skip,
],
SegmentType.poi_highlight => [ActionType.poi],
SegmentType.filler => [
ActionType.skip,
ActionType.mute,
],
SegmentType.exclusive_access => [ActionType.full],
};
}
extension SegmentTypeExt on SegmentType { extension SegmentTypeExt on SegmentType {
/// from https://github.com/hanydd/BilibiliSponsorBlock/blob/master/public/_locales/zh_CN/messages.json /// from https://github.com/hanydd/BilibiliSponsorBlock/blob/master/public/_locales/zh_CN/messages.json
String get title => [ String get title => [

View File

@@ -1,3 +1,4 @@
import 'package:PiliPlus/pages/common/common_slide_page.dart';
import 'package:PiliPlus/pages/search/widgets/search_text.dart'; import 'package:PiliPlus/pages/search/widgets/search_text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:PiliPlus/common/widgets/stat/danmu.dart'; import 'package:PiliPlus/common/widgets/stat/danmu.dart';
@@ -6,7 +7,7 @@ import 'package:get/get.dart';
import '../../../../utils/utils.dart'; import '../../../../utils/utils.dart';
class IntroDetail extends StatelessWidget { class IntroDetail extends CommonSlidePage {
final dynamic bangumiDetail; final dynamic bangumiDetail;
final dynamic videoTags; final dynamic videoTags;
@@ -17,11 +18,17 @@ class IntroDetail extends StatelessWidget {
}); });
@override @override
Widget build(BuildContext context) { State<IntroDetail> createState() => _IntroDetailState();
TextStyle smallTitle = TextStyle( }
fontSize: 12,
color: Theme.of(context).colorScheme.onSurface, class _IntroDetailState extends CommonSlidePageState<IntroDetail> {
); late final TextStyle smallTitle = TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurface,
);
@override
Widget get buildPage {
return Padding( return Padding(
padding: const EdgeInsets.only(left: 14, right: 14), padding: const EdgeInsets.only(left: 14, right: 14),
child: Column( child: Column(
@@ -46,99 +53,103 @@ class IntroDetail extends StatelessWidget {
), ),
), ),
Expanded( Expanded(
child: SingleChildScrollView( child: enableSlide ? slideList() : buildList,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(
bangumiDetail!.title,
style: const TextStyle(
fontSize: 16,
),
),
const SizedBox(height: 4),
Row(
children: [
statView(
context: context,
theme: 'gray',
view: bangumiDetail!.stat!['views'],
size: 'medium',
),
const SizedBox(width: 6),
statDanMu(
context: context,
theme: 'gray',
danmu: bangumiDetail!.stat!['danmakus'],
size: 'medium',
),
],
),
const SizedBox(height: 4),
Row(
children: [
Text(
bangumiDetail!.areas!.first['name'],
style: smallTitle,
),
const SizedBox(width: 6),
Text(
bangumiDetail!.publish!['pub_time_show'],
style: smallTitle,
),
const SizedBox(width: 6),
Text(
bangumiDetail!.newEp!['desc'],
style: smallTitle,
),
],
),
const SizedBox(height: 20),
Text(
'简介:',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 4),
SelectableText(
'${bangumiDetail!.evaluate!}',
style: smallTitle.copyWith(fontSize: 13),
),
const SizedBox(height: 20),
Text(
'声优:',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 4),
SelectableText(
bangumiDetail.actors,
style: smallTitle.copyWith(fontSize: 13),
),
if (videoTags is List && videoTags.isNotEmpty) ...[
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: (videoTags as List)
.map(
(item) => SearchText(
fontSize: 13,
text: item['tag_name'],
onTap: (_) => Get.toNamed('/searchResult',
parameters: {'keyword': item['tag_name']}),
onLongPress: (_) =>
Utils.copyText(item['tag_name']),
),
)
.toList(),
)
],
SizedBox(height: MediaQuery.of(context).padding.bottom + 20)
],
),
),
) )
], ],
), ),
); );
} }
@override
Widget get buildList => SingleChildScrollView(
controller: ScrollController(),
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(
widget.bangumiDetail!.title,
style: const TextStyle(
fontSize: 16,
),
),
const SizedBox(height: 4),
Row(
children: [
statView(
context: context,
theme: 'gray',
view: widget.bangumiDetail!.stat!['views'],
size: 'medium',
),
const SizedBox(width: 6),
statDanMu(
context: context,
theme: 'gray',
danmu: widget.bangumiDetail!.stat!['danmakus'],
size: 'medium',
),
],
),
const SizedBox(height: 4),
Row(
children: [
Text(
widget.bangumiDetail!.areas!.first['name'],
style: smallTitle,
),
const SizedBox(width: 6),
Text(
widget.bangumiDetail!.publish!['pub_time_show'],
style: smallTitle,
),
const SizedBox(width: 6),
Text(
widget.bangumiDetail!.newEp!['desc'],
style: smallTitle,
),
],
),
const SizedBox(height: 20),
Text(
'简介:',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 4),
SelectableText(
'${widget.bangumiDetail!.evaluate!}',
style: smallTitle.copyWith(fontSize: 13),
),
const SizedBox(height: 20),
Text(
'声优:',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 4),
SelectableText(
widget.bangumiDetail.actors,
style: smallTitle.copyWith(fontSize: 13),
),
if (widget.videoTags is List && widget.videoTags.isNotEmpty) ...[
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: (widget.videoTags as List)
.map(
(item) => SearchText(
fontSize: 13,
text: item['tag_name'],
onTap: (_) => Get.toNamed('/searchResult',
parameters: {'keyword': item['tag_name']}),
onLongPress: (_) => Utils.copyText(item['tag_name']),
),
)
.toList(),
)
],
SizedBox(height: MediaQuery.of(context).padding.bottom + 20)
],
),
);
} }

View File

@@ -33,9 +33,6 @@ abstract class CommonPublishPage extends StatefulWidget {
final int? imageLengthLimit; final int? imageLengthLimit;
final ValueChanged<String>? onSave; final ValueChanged<String>? onSave;
final bool autofocus; final bool autofocus;
@override
State<CommonPublishPage> createState();
} }
abstract class CommonPublishPageState<T extends CommonPublishPage> abstract class CommonPublishPageState<T extends CommonPublishPage>

View File

@@ -0,0 +1,91 @@
import 'package:PiliPlus/utils/storage.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
abstract class CommonSlidePage extends StatefulWidget {
const CommonSlidePage({super.key, this.enableSlide});
final bool? enableSlide;
}
abstract class CommonSlidePageState<T extends CommonSlidePage>
extends State<T> {
Offset? downPos;
bool? isSliding;
late double padding = 0.0;
late final enableSlide =
widget.enableSlide != false && GStorage.slideDismissReplyPage;
@override
Widget build(BuildContext context) {
return enableSlide
? Padding(
padding: EdgeInsets.only(top: padding),
child: buildPage,
)
: buildPage;
}
Widget get buildPage;
Widget get buildList => throw UnimplementedError();
Widget slideList([Widget? buildList]) => GestureDetector(
onPanDown: (event) {
if (event.localPosition.dx > 30) {
isSliding = false;
} else {
downPos = event.localPosition;
}
},
onPanUpdate: (event) {
if (isSliding == false) {
return;
} else if (isSliding == null) {
if (downPos != null) {
Offset cumulativeDelta = event.localPosition - downPos!;
if (cumulativeDelta.dx.abs() >= cumulativeDelta.dy.abs()) {
isSliding = true;
setState(() {
padding = event.localPosition.dx;
});
} else {
isSliding = false;
}
}
} else if (isSliding == true) {
setState(() {
padding = event.localPosition.dx;
});
}
},
onPanCancel: () {
if (isSliding == true) {
if (padding >= 100) {
Get.back();
} else {
setState(() {
padding = 0;
});
}
}
downPos = null;
isSliding = null;
},
onPanEnd: (event) {
if (isSliding == true) {
if (padding >= 100) {
Get.back();
} else {
setState(() {
padding = 0;
});
}
}
downPos = null;
isSliding = null;
},
child: buildList ?? this.buildList,
);
}

View File

@@ -2,9 +2,6 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'dart:ui'; import 'dart:ui';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/icon_button.dart';
import 'package:PiliPlus/common/widgets/loading_widget.dart';
import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/common/widgets/pair.dart';
import 'package:PiliPlus/common/widgets/segment_progress_bar.dart'; import 'package:PiliPlus/common/widgets/segment_progress_bar.dart';
import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/init.dart';
@@ -21,9 +18,9 @@ import 'package:PiliPlus/models/video_detail_res.dart';
import 'package:PiliPlus/pages/search/widgets/search_text.dart'; import 'package:PiliPlus/pages/search/widgets/search_text.dart';
import 'package:PiliPlus/pages/video/detail/introduction/controller.dart'; import 'package:PiliPlus/pages/video/detail/introduction/controller.dart';
import 'package:PiliPlus/pages/video/detail/note/note_list_page.dart'; import 'package:PiliPlus/pages/video/detail/note/note_list_page.dart';
import 'package:PiliPlus/pages/video/detail/post_panel/post_panel.dart';
import 'package:PiliPlus/pages/video/detail/related/controller.dart'; import 'package:PiliPlus/pages/video/detail/related/controller.dart';
import 'package:PiliPlus/pages/video/detail/reply/controller.dart'; import 'package:PiliPlus/pages/video/detail/reply/controller.dart';
import 'package:PiliPlus/pages/video/detail/view_v.dart' show ViewPointsPage;
import 'package:PiliPlus/pages/video/detail/widgets/send_danmaku_panel.dart'; import 'package:PiliPlus/pages/video/detail/widgets/send_danmaku_panel.dart';
import 'package:PiliPlus/pages/video/detail/widgets/watch_later_list.dart'; import 'package:PiliPlus/pages/video/detail/widgets/watch_later_list.dart';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/extension.dart';
@@ -33,7 +30,6 @@ import 'package:easy_debounce/easy_throttle.dart';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:floating/floating.dart'; import 'package:floating/floating.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/constants.dart';
@@ -47,7 +43,6 @@ import 'package:PiliPlus/utils/utils.dart';
import 'package:PiliPlus/utils/video_utils.dart'; import 'package:PiliPlus/utils/video_utils.dart';
import 'package:get/get_navigation/src/dialog/dialog_route.dart'; import 'package:get/get_navigation/src/dialog/dialog_route.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import '../../../utils/id_utils.dart'; import '../../../utils/id_utils.dart';
import 'widgets/header_control.dart'; import 'widgets/header_control.dart';
@@ -400,6 +395,7 @@ class VideoDetailController extends GetxController
showMediaListPanel(context) { showMediaListPanel(context) {
if (mediaList.isNotEmpty) { if (mediaList.isNotEmpty) {
childKey.currentState?.showBottomSheet( childKey.currentState?.showBottomSheet(
shape: const RoundedRectangleBorder(),
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
(context) => MediaListPanel( (context) => MediaListPanel(
mediaList: mediaList, mediaList: mediaList,
@@ -463,7 +459,7 @@ class VideoDetailController extends GetxController
List<Color>? _blockColor; List<Color>? _blockColor;
RxList<SegmentModel> segmentList = <SegmentModel>[].obs; RxList<SegmentModel> segmentList = <SegmentModel>[].obs;
List<Segment> viewPointList = <Segment>[]; List<Segment> viewPointList = <Segment>[];
List<Segment>? _segmentProgressList; List<Segment>? segmentProgressList;
Color _getColor(SegmentType segment) => Color _getColor(SegmentType segment) =>
_blockColor?[segment.index] ?? segment.color; _blockColor?[segment.index] ?? segment.color;
late RxString videoLabel = ''.obs; late RxString videoLabel = ''.obs;
@@ -481,7 +477,7 @@ class VideoDetailController extends GetxController
'userID': GStorage.blockUserID, 'userID': GStorage.blockUserID,
'type': type, 'type': type,
}, },
options: _options, options: options,
) )
.then((res) { .then((res) {
SmartDialog.showToast(res.statusCode == 200 ? '投票成功' : '投票失败'); SmartDialog.showToast(res.statusCode == 200 ? '投票成功' : '投票失败');
@@ -510,7 +506,7 @@ class VideoDetailController extends GetxController
'userID': GStorage.blockUserID, 'userID': GStorage.blockUserID,
'category': item.name, 'category': item.name,
}, },
options: _options, options: options,
) )
.then((res) { .then((res) {
SmartDialog.showToast( SmartDialog.showToast(
@@ -705,25 +701,25 @@ class VideoDetailController extends GetxController
); );
} }
Options get _options => Options(extra: {'clearCookie': true}); Options get options => Options(extra: {'clearCookie': true});
Future _querySponsorBlock() async { Future _querySponsorBlock() async {
positionSubscription?.cancel(); positionSubscription?.cancel();
videoLabel.value = ''; videoLabel.value = '';
segmentList.clear(); segmentList.clear();
_segmentProgressList = null; segmentProgressList = null;
dynamic result = await Request().get( dynamic result = await Request().get(
'${GStorage.blockServer}/api/skipSegments', '${GStorage.blockServer}/api/skipSegments',
queryParameters: { queryParameters: {
'videoID': bvid, 'videoID': bvid,
'cid': cid.value, 'cid': cid.value,
}, },
options: _options, options: options,
); );
_handleSBData(result); handleSBData(result);
} }
void _handleSBData(result) { void handleSBData(result) {
if (result.data is List && result.data.isNotEmpty) { if (result.data is List && result.data.isNotEmpty) {
try { try {
List<String> list = List<String> list =
@@ -767,8 +763,8 @@ class VideoDetailController extends GetxController
).toList()); ).toList());
// _segmentProgressList // _segmentProgressList
_segmentProgressList ??= <Segment>[]; segmentProgressList ??= <Segment>[];
_segmentProgressList!.addAll(segmentList.map((item) { segmentProgressList!.addAll(segmentList.map((item) {
double start = (item.segment.first / ((data.timeLength ?? 0) / 1000)) double start = (item.segment.first / ((data.timeLength ?? 0) / 1000))
.clamp(0.0, 1.0); .clamp(0.0, 1.0);
double end = (item.segment.second / ((data.timeLength ?? 0) / 1000)) double end = (item.segment.second / ((data.timeLength ?? 0) / 1000))
@@ -789,7 +785,7 @@ class VideoDetailController extends GetxController
: -1; : -1;
} }
void _initSkip() { void initSkip() {
if (segmentList.isNotEmpty) { if (segmentList.isNotEmpty) {
positionSubscription = plPlayerController positionSubscription = plPlayerController
.videoPlayerController?.stream.position .videoPlayerController?.stream.position
@@ -920,7 +916,7 @@ class VideoDetailController extends GetxController
Request().post( Request().post(
'${GStorage.blockServer}/api/viewedVideoSponsorTime', '${GStorage.blockServer}/api/viewedVideoSponsorTime',
queryParameters: {'UUID': item.UUID}, queryParameters: {'UUID': item.UUID},
options: _options, options: options,
); );
} }
} catch (e) { } catch (e) {
@@ -1073,7 +1069,7 @@ class VideoDetailController extends GetxController
'referer': HttpString.baseUrl 'referer': HttpString.baseUrl
}, },
), ),
segmentList: _segmentProgressList, segmentList: segmentProgressList,
viewPointList: viewPointList, viewPointList: viewPointList,
vttSubtitles: _vttSubtitles, vttSubtitles: _vttSubtitles,
vttSubtitlesIndex: vttSubtitlesIndex, vttSubtitlesIndex: vttSubtitlesIndex,
@@ -1102,7 +1098,7 @@ class VideoDetailController extends GetxController
}, },
); );
_initSkip(); initSkip();
if (vttSubtitlesIndex == null) { if (vttSubtitlesIndex == null) {
_getSubtitle(); _getSubtitle();
@@ -1337,601 +1333,24 @@ class VideoDetailController extends GetxController
} }
if (plPlayerController.isFullScreen.value) { if (plPlayerController.isFullScreen.value) {
Utils.showFSSheet( Utils.showFSSheet(
child: _postPanel(), child: PostPanel(
isFullScreen: plPlayerController.isFullScreen.value, enableSlide: false,
videoDetailController: this,
plPlayerController: plPlayerController,
),
isFullScreen: () => plPlayerController.isFullScreen.value,
); );
} else { } else {
childKey.currentState?.showBottomSheet( childKey.currentState?.showBottomSheet(
enableDrag: false,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
(context) => GStorage.collapsibleVideoPage (context) => PostPanel(
? ViewPointsPage(child: _postPanel()) videoDetailController: this,
: _postPanel(), plPlayerController: plPlayerController,
),
); );
} }
} }
Widget _postPanel() => StatefulBuilder(
builder: (context, setState) {
void updateSegment({
required bool isFirst,
required int index,
required int value,
}) {
if (isFirst) {
list![index].segment.first = value;
} else {
list![index].segment.second = value;
}
if (list![index].category == SegmentType.poi_highlight ||
list![index].actionType == ActionType.full) {
list![index].segment.second = value;
}
}
List<Widget> segmentWidget({
required int index,
required bool isFirst,
}) {
String value = Utils.timeFormat(isFirst
? list![index].segment.first
: list![index].segment.second);
return [
Text(
'${isFirst ? '开始' : '结束'}: $value',
),
const SizedBox(width: 5),
iconButton(
context: context,
size: 26,
tooltip: '使用当前位置时间',
icon: Icons.my_location,
onPressed: () {
setState(() {
updateSegment(
isFirst: isFirst,
index: index,
value: plPlayerController.positionSeconds.value,
);
});
},
),
const SizedBox(width: 5),
iconButton(
context: context,
size: 26,
tooltip: '编辑',
icon: Icons.edit,
onPressed: () {
showDialog(
context: context,
builder: (context) {
String initV = value;
return AlertDialog(
content: TextFormField(
initialValue: value,
autofocus: true,
onChanged: (value) {
initV = value;
},
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'[\d:]+'),
),
],
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline),
),
),
TextButton(
onPressed: () => Get.back(result: initV),
child: Text('确定'),
),
],
);
},
).then((res) {
if (res != null) {
try {
List<int> split = (res as String)
.split(':')
.toList()
.reversed
.toList()
.map((e) => int.parse(e))
.toList();
int duration = 0;
for (int i = 0; i < split.length; i++) {
duration += split[i] * pow(60, i).toInt();
}
if (duration <=
plPlayerController
.durationSeconds.value.inSeconds) {
setState(() {
updateSegment(
isFirst: isFirst,
index: index,
value: duration,
);
});
}
} catch (e) {
debugPrint(e.toString());
}
}
});
},
),
];
}
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
automaticallyImplyLeading: false,
titleSpacing: 16,
title: const Text('提交片段'),
actions: [
iconButton(
context: context,
tooltip: '添加片段',
onPressed: () {
setState(() {
list?.insert(
0,
PostSegmentModel(
segment: Pair(
first: 0,
second: plPlayerController.positionSeconds.value,
),
category: SegmentType.sponsor,
actionType: ActionType.skip,
),
);
});
},
icon: Icons.add,
),
const SizedBox(width: 10),
iconButton(
context: context,
tooltip: '关闭',
onPressed: Get.back,
icon: Icons.close,
),
const SizedBox(width: 16),
],
),
body: list?.isNotEmpty == true
? Stack(
children: [
SingleChildScrollView(
controller: ScrollController(),
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
children: [
...List.generate(
list!.length,
(index) => Stack(
children: [
Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 5,
),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.onInverseSurface,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
if (list![index].actionType !=
ActionType.full) ...[
Wrap(
runSpacing: 8,
spacing: 16,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: segmentWidget(
isFirst: true,
index: index,
),
),
if (list![index].category !=
SegmentType.poi_highlight)
Row(
mainAxisSize:
MainAxisSize.min,
children: segmentWidget(
isFirst: false,
index: index,
),
),
],
),
const SizedBox(height: 8),
],
Wrap(
runSpacing: 8,
spacing: 16,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('分类: '),
PopupMenuButton(
initialValue:
list![index].category,
onSelected: (item) async {
list![index].category =
item;
List<ActionType>
constraintList =
_segmentType2ActionType(
item);
if (constraintList
.contains(list![index]
.actionType)
.not) {
list![index].actionType =
constraintList.first;
}
switch (item) {
case SegmentType
.poi_highlight:
updateSegment(
isFirst: false,
index: index,
value: list![index]
.segment
.first,
);
break;
case SegmentType
.exclusive_access:
updateSegment(
isFirst: true,
index: index,
value: 0,
);
break;
case _:
}
setState(() {});
},
itemBuilder: (context) =>
SegmentType.values
.map((item) =>
PopupMenuItem<
SegmentType>(
value: item,
child: Text(
item.title),
))
.toList(),
child: Row(
mainAxisSize:
MainAxisSize.min,
children: [
Text(
list![index]
.category
.title,
style: TextStyle(
height: 1,
fontSize: 14,
color:
Theme.of(context)
.colorScheme
.secondary,
),
strutStyle: StrutStyle(
height: 1,
leading: 0,
),
),
Icon(
MdiIcons
.unfoldMoreHorizontal,
size: MediaQuery
.textScalerOf(
context)
.scale(14),
color: Theme.of(context)
.colorScheme
.secondary,
),
],
),
),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('行为类别: '),
PopupMenuButton(
initialValue:
list![index].actionType,
onSelected: (item) async {
list![index].actionType =
item;
if (item ==
ActionType.full) {
updateSegment(
isFirst: true,
index: index,
value: 0,
);
}
setState(() {});
},
itemBuilder: (context) =>
ActionType.values
.map(
(item) =>
PopupMenuItem<
ActionType>(
enabled: _segmentType2ActionType(
list![index]
.category)
.contains(
item),
value: item,
child: Text(
item.title),
),
)
.toList(),
child: Row(
mainAxisSize:
MainAxisSize.min,
children: [
Text(
list![index]
.actionType
.title,
style: TextStyle(
height: 1,
fontSize: 14,
color:
Theme.of(context)
.colorScheme
.secondary,
),
strutStyle: StrutStyle(
height: 1,
leading: 0,
),
),
Icon(
MdiIcons
.unfoldMoreHorizontal,
size: MediaQuery
.textScalerOf(
context)
.scale(14),
color: Theme.of(context)
.colorScheme
.secondary,
),
],
),
),
],
),
],
),
],
),
),
Positioned(
top: 10,
right: 21,
child: iconButton(
context: context,
size: 26,
tooltip: '移除',
icon: Icons.clear,
onPressed: () {
setState(() {
list!.removeAt(index);
});
},
),
),
],
),
),
SizedBox(
height: 88 + MediaQuery.paddingOf(context).bottom,
),
],
),
),
Positioned(
right: 16,
bottom: 16 + MediaQuery.paddingOf(context).bottom,
child: FloatingActionButton(
tooltip: '提交',
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('确定无误再提交'),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline,
),
),
),
TextButton(
onPressed: () {
Get.back();
Request()
.post(
'${GStorage.blockServer}/api/skipSegments',
queryParameters: {
'videoID': bvid,
'cid': cid.value,
'userID': GStorage.blockUserID,
'userAgent': Constants.userAgent,
'videoDuration': plPlayerController
.durationSeconds.value.inSeconds,
},
data: {
'segments': list!
.map(
(item) => {
'segment': [
item.segment.first,
item.segment.second,
],
'category':
item.category.name,
'actionType':
item.actionType.name,
},
)
.toList(),
},
options: _options,
)
.then(
(res) {
if (res.statusCode == 200) {
Get.back();
SmartDialog.showToast('提交成功');
list?.clear();
_handleSBData(res);
plPlayerController
.segmentList.value =
_segmentProgressList ??
<Segment>[];
if (positionSubscription == null) {
_initSkip();
}
} else {
SmartDialog.showToast(
'提交失败: ${{
400: '参数错误',
403: '被自动审核机制拒绝',
429: '重复提交太快',
409: '重复提交'
}[res.statusCode]}',
);
}
},
);
},
child: const Text('确定提交'),
),
],
),
);
},
child: Icon(Icons.check),
),
)
],
)
: errorWidget(),
);
},
);
// List<SegmentType> _actionType2SegmentType(ActionType actionType) {
// return switch (actionType) {
// ActionType.skip => [
// SegmentType.sponsor,
// SegmentType.selfpromo,
// SegmentType.interaction,
// SegmentType.intro,
// SegmentType.outro,
// SegmentType.preview,
// SegmentType.filler,
// ],
// ActionType.mute => [
// SegmentType.sponsor,
// SegmentType.selfpromo,
// SegmentType.interaction,
// SegmentType.intro,
// SegmentType.outro,
// SegmentType.preview,
// SegmentType.music_offtopic,
// SegmentType.filler,
// ],
// ActionType.full => [
// SegmentType.sponsor,
// SegmentType.selfpromo,
// SegmentType.exclusive_access,
// ],
// ActionType.poi => [
// SegmentType.poi_highlight,
// ],
// };
// }
List<ActionType> _segmentType2ActionType(SegmentType segmentType) {
return switch (segmentType) {
SegmentType.sponsor => [
ActionType.skip,
ActionType.mute,
ActionType.full
],
SegmentType.selfpromo => [
ActionType.skip,
ActionType.mute,
ActionType.full
],
SegmentType.interaction => [
ActionType.skip,
ActionType.mute,
],
SegmentType.intro => [
ActionType.skip,
ActionType.mute,
],
SegmentType.outro => [
ActionType.skip,
ActionType.mute,
],
SegmentType.preview => [
ActionType.skip,
ActionType.mute,
],
SegmentType.music_offtopic => [
ActionType.skip,
],
SegmentType.poi_highlight => [ActionType.poi],
SegmentType.filler => [
ActionType.skip,
ActionType.mute,
],
SegmentType.exclusive_access => [ActionType.full],
};
}
late List<Map<String, String>> _vttSubtitles = <Map<String, String>>[]; late List<Map<String, String>> _vttSubtitles = <Map<String, String>>[];
int? vttSubtitlesIndex; int? vttSubtitlesIndex;
late bool showVP = true; late bool showVP = true;
@@ -2139,7 +1558,7 @@ class VideoDetailController extends GetxController
positionSubscription?.cancel(); positionSubscription?.cancel();
videoLabel.value = ''; videoLabel.value = '';
segmentList.clear(); segmentList.clear();
_segmentProgressList = null; segmentProgressList = null;
} }
// interactive video // interactive video
@@ -2177,11 +1596,14 @@ class VideoDetailController extends GetxController
} }
} }
void showNoteList() async { void showNoteList(BuildContext context) async {
if (plPlayerController.isFullScreen.value) { if (plPlayerController.isFullScreen.value) {
Utils.showFSSheet( Utils.showFSSheet(
child: NoteListPage(oid: oid.value), child: NoteListPage(
isFullScreen: plPlayerController.isFullScreen.value, oid: oid.value,
enableSlide: false,
),
isFullScreen: () => plPlayerController.isFullScreen.value,
); );
} else { } else {
childKey.currentState?.showBottomSheet( childKey.currentState?.showBottomSheet(

View File

@@ -35,13 +35,11 @@ class VideoIntroPanel extends StatefulWidget {
super.key, super.key,
required this.heroTag, required this.heroTag,
required this.showAiBottomSheet, required this.showAiBottomSheet,
required this.showIntroDetail,
required this.showEpisodes, required this.showEpisodes,
required this.onShowMemberPage, required this.onShowMemberPage,
}); });
final String heroTag; final String heroTag;
final Function showAiBottomSheet; final Function showAiBottomSheet;
final Function showIntroDetail;
final Function showEpisodes; final Function showEpisodes;
final ValueChanged onShowMemberPage; final ValueChanged onShowMemberPage;
@@ -75,10 +73,6 @@ class _VideoIntroPanelState extends State<VideoIntroPanel>
videoIntroController: videoIntroController, videoIntroController: videoIntroController,
heroTag: widget.heroTag, heroTag: widget.heroTag,
showAiBottomSheet: widget.showAiBottomSheet, showAiBottomSheet: widget.showAiBottomSheet,
showIntroDetail: () => widget.showIntroDetail(
videoIntroController.videoDetail.value,
videoIntroController.videoTags,
),
showEpisodes: widget.showEpisodes, showEpisodes: widget.showEpisodes,
onShowMemberPage: widget.onShowMemberPage, onShowMemberPage: widget.onShowMemberPage,
) )
@@ -88,10 +82,6 @@ class _VideoIntroPanelState extends State<VideoIntroPanel>
videoIntroController: videoIntroController, videoIntroController: videoIntroController,
heroTag: widget.heroTag, heroTag: widget.heroTag,
showAiBottomSheet: widget.showAiBottomSheet, showAiBottomSheet: widget.showAiBottomSheet,
showIntroDetail: () => widget.showIntroDetail(
videoIntroController.videoDetail.value,
videoIntroController.videoTags,
),
showEpisodes: widget.showEpisodes, showEpisodes: widget.showEpisodes,
onShowMemberPage: widget.onShowMemberPage, onShowMemberPage: widget.onShowMemberPage,
), ),
@@ -103,7 +93,6 @@ class VideoInfo extends StatefulWidget {
final bool loadingStatus; final bool loadingStatus;
final String heroTag; final String heroTag;
final Function showAiBottomSheet; final Function showAiBottomSheet;
final Function showIntroDetail;
final Function showEpisodes; final Function showEpisodes;
final ValueChanged onShowMemberPage; final ValueChanged onShowMemberPage;
final VideoIntroController videoIntroController; final VideoIntroController videoIntroController;
@@ -113,7 +102,6 @@ class VideoInfo extends StatefulWidget {
this.loadingStatus = false, this.loadingStatus = false,
required this.heroTag, required this.heroTag,
required this.showAiBottomSheet, required this.showAiBottomSheet,
required this.showIntroDetail,
required this.showEpisodes, required this.showEpisodes,
required this.onShowMemberPage, required this.onShowMemberPage,
required this.videoIntroController, required this.videoIntroController,

View File

@@ -4,14 +4,20 @@ import 'package:PiliPlus/common/widgets/loading_widget.dart';
import 'package:PiliPlus/common/widgets/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/pages/common/common_slide_page.dart';
import 'package:PiliPlus/pages/video/detail/note/note_list_page_ctr.dart'; import 'package:PiliPlus/pages/video/detail/note/note_list_page_ctr.dart';
import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/app_scheme.dart';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/extension.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class NoteListPage extends StatefulWidget { class NoteListPage extends CommonSlidePage {
const NoteListPage({super.key, this.oid, this.upperMid}); const NoteListPage({
super.key,
super.enableSlide,
this.oid,
this.upperMid,
});
final dynamic oid; final dynamic oid;
final dynamic upperMid; final dynamic upperMid;
@@ -20,7 +26,7 @@ class NoteListPage extends StatefulWidget {
State<NoteListPage> createState() => _NoteListPageState(); State<NoteListPage> createState() => _NoteListPageState();
} }
class _NoteListPageState extends State<NoteListPage> { class _NoteListPageState extends CommonSlidePageState<NoteListPage> {
late final _controller = Get.put( late final _controller = Get.put(
NoteListPageCtr(oid: widget.oid, upperMid: widget.upperMid), NoteListPageCtr(oid: widget.oid, upperMid: widget.upperMid),
); );
@@ -32,37 +38,37 @@ class _NoteListPageState extends State<NoteListPage> {
} }
@override @override
Widget build(BuildContext context) { Widget get buildPage => Scaffold(
return Scaffold( appBar: AppBar(
appBar: AppBar( automaticallyImplyLeading: false,
automaticallyImplyLeading: false, titleSpacing: 16,
titleSpacing: 16, toolbarHeight: 45,
toolbarHeight: 45, title: Obx(
title: Obx( () => Text(
() => Text( '笔记${_controller.count.value == -1 ? '' : '(${_controller.count.value})'}'),
'笔记${_controller.count.value == -1 ? '' : '(${_controller.count.value})'}'),
),
bottom: PreferredSize(
preferredSize: Size.fromHeight(1),
child: Divider(
height: 1,
color: Theme.of(context).colorScheme.outline.withOpacity(0.1),
), ),
), bottom: PreferredSize(
actions: [ preferredSize: Size.fromHeight(1),
iconButton( child: Divider(
context: context, height: 1,
tooltip: '关闭', color: Theme.of(context).colorScheme.outline.withOpacity(0.1),
icon: Icons.clear, ),
onPressed: Get.back,
size: 32,
), ),
const SizedBox(width: 16), actions: [
], iconButton(
), context: context,
body: Obx(() => _buildBody(_controller.loadingState.value)), tooltip: '关闭',
); icon: Icons.clear,
} onPressed: Get.back,
size: 32,
),
const SizedBox(width: 16),
],
),
body: enableSlide
? slideList(Obx(() => _buildBody(_controller.loadingState.value)))
: Obx(() => _buildBody(_controller.loadingState.value)),
);
Widget _buildBody(LoadingState loadingState) { Widget _buildBody(LoadingState loadingState) {
return switch (loadingState) { return switch (loadingState) {

View File

@@ -0,0 +1,548 @@
import 'dart:math';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/icon_button.dart';
import 'package:PiliPlus/common/widgets/loading_widget.dart';
import 'package:PiliPlus/common/widgets/pair.dart';
import 'package:PiliPlus/common/widgets/segment_progress_bar.dart';
import 'package:PiliPlus/http/init.dart';
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/pages/common/common_slide_page.dart';
import 'package:PiliPlus/pages/video/detail/index.dart';
import 'package:PiliPlus/plugin/pl_player/index.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
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';
class PostPanel extends CommonSlidePage {
const PostPanel({
super.key,
super.enableSlide,
required this.videoDetailController,
required this.plPlayerController,
});
final VideoDetailController videoDetailController;
final PlPlayerController plPlayerController;
@override
State<PostPanel> createState() => _PostPanelState();
}
class _PostPanelState extends CommonSlidePageState<PostPanel> {
late bool _isInit = true;
VideoDetailController get videoDetailController =>
widget.videoDetailController;
PlPlayerController get plPlayerController => widget.plPlayerController;
List<PostSegmentModel>? get list => videoDetailController.list;
@override
void initState() {
super.initState();
if (enableSlide && GStorage.collapsibleVideoPage) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_isInit = false;
});
}
});
}
}
@override
Widget build(BuildContext context) {
if (enableSlide && GStorage.collapsibleVideoPage && _isInit) {
return CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
);
}
return enableSlide
? Padding(
padding: EdgeInsets.only(top: padding),
child: buildPage,
)
: buildPage;
}
@override
Widget get buildPage => Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
automaticallyImplyLeading: false,
titleSpacing: 16,
title: const Text('提交片段'),
actions: [
iconButton(
context: context,
tooltip: '添加片段',
onPressed: () {
setState(() {
list?.insert(
0,
PostSegmentModel(
segment: Pair(
first: 0,
second: plPlayerController.positionSeconds.value,
),
category: SegmentType.sponsor,
actionType: ActionType.skip,
),
);
});
},
icon: Icons.add,
),
const SizedBox(width: 10),
iconButton(
context: context,
tooltip: '关闭',
onPressed: Get.back,
icon: Icons.close,
),
const SizedBox(width: 16),
],
),
body: enableSlide ? slideList() : buildList,
);
@override
Widget get buildList => list?.isNotEmpty == true
? Stack(
children: [
SingleChildScrollView(
controller: ScrollController(),
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
children: [
...List.generate(
list!.length,
(index) => Stack(
children: [
Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 5,
),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.onInverseSurface,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (list![index].actionType !=
ActionType.full) ...[
Wrap(
runSpacing: 8,
spacing: 16,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: segmentWidget(
isFirst: true,
index: index,
),
),
if (list![index].category !=
SegmentType.poi_highlight)
Row(
mainAxisSize: MainAxisSize.min,
children: segmentWidget(
isFirst: false,
index: index,
),
),
],
),
const SizedBox(height: 8),
],
Wrap(
runSpacing: 8,
spacing: 16,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('分类: '),
PopupMenuButton<SegmentType>(
initialValue: list![index].category,
onSelected: (item) async {
list![index].category = item;
List<ActionType> constraintList =
segmentType2ActionType(item);
if (constraintList
.contains(list![index].actionType)
.not) {
list![index].actionType =
constraintList.first;
}
switch (item) {
case SegmentType.poi_highlight:
updateSegment(
isFirst: false,
index: index,
value:
list![index].segment.first,
);
break;
case SegmentType.exclusive_access:
updateSegment(
isFirst: true,
index: index,
value: 0,
);
break;
case _:
}
setState(() {});
},
itemBuilder: (context) => SegmentType
.values
.map((item) =>
PopupMenuItem<SegmentType>(
value: item,
child: Text(item.title),
))
.toList(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
list![index].category.title,
style: TextStyle(
height: 1,
fontSize: 14,
color: Theme.of(context)
.colorScheme
.secondary,
),
strutStyle: StrutStyle(
height: 1,
leading: 0,
),
),
Icon(
MdiIcons.unfoldMoreHorizontal,
size: MediaQuery.textScalerOf(
context)
.scale(14),
color: Theme.of(context)
.colorScheme
.secondary,
),
],
),
),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('行为类别: '),
PopupMenuButton<ActionType>(
initialValue: list![index].actionType,
onSelected: (item) async {
list![index].actionType = item;
if (item == ActionType.full) {
updateSegment(
isFirst: true,
index: index,
value: 0,
);
}
setState(() {});
},
itemBuilder: (context) => ActionType
.values
.map(
(item) =>
PopupMenuItem<ActionType>(
enabled: segmentType2ActionType(
list![index].category)
.contains(item),
value: item,
child: Text(item.title),
),
)
.toList(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
list![index].actionType.title,
style: TextStyle(
height: 1,
fontSize: 14,
color: Theme.of(context)
.colorScheme
.secondary,
),
strutStyle: StrutStyle(
height: 1,
leading: 0,
),
),
Icon(
MdiIcons.unfoldMoreHorizontal,
size: MediaQuery.textScalerOf(
context)
.scale(14),
color: Theme.of(context)
.colorScheme
.secondary,
),
],
),
),
],
),
],
),
],
),
),
Positioned(
top: 10,
right: 21,
child: iconButton(
context: context,
size: 26,
tooltip: '移除',
icon: Icons.clear,
onPressed: () {
setState(() {
list!.removeAt(index);
});
},
),
),
],
),
),
SizedBox(
height: 88 + MediaQuery.paddingOf(context).bottom,
),
],
),
),
Positioned(
right: 16,
bottom: 16 + MediaQuery.paddingOf(context).bottom,
child: FloatingActionButton(
tooltip: '提交',
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('确定无误再提交'),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
),
TextButton(
onPressed: () {
Get.back();
Request()
.post(
'${GStorage.blockServer}/api/skipSegments',
queryParameters: {
'videoID': videoDetailController.bvid,
'cid': videoDetailController.cid.value,
'userID': GStorage.blockUserID,
'userAgent': Constants.userAgent,
'videoDuration': plPlayerController
.durationSeconds.value.inSeconds,
},
data: {
'segments': list!
.map(
(item) => {
'segment': [
item.segment.first,
item.segment.second,
],
'category': item.category.name,
'actionType': item.actionType.name,
},
)
.toList(),
},
options: videoDetailController.options,
)
.then(
(res) {
if (res.statusCode == 200) {
Get.back();
SmartDialog.showToast('提交成功');
list?.clear();
videoDetailController.handleSBData(res);
plPlayerController.segmentList.value =
videoDetailController
.segmentProgressList ??
<Segment>[];
if (videoDetailController
.positionSubscription ==
null) {
videoDetailController.initSkip();
}
} else {
SmartDialog.showToast(
'提交失败: ${{
400: '参数错误',
403: '被自动审核机制拒绝',
429: '重复提交太快',
409: '重复提交'
}[res.statusCode] ?? res.statusCode}',
);
}
},
);
},
child: const Text('确定提交'),
),
],
),
);
},
child: Icon(Icons.check),
),
)
],
)
: errorWidget();
void updateSegment({
required bool isFirst,
required int index,
required int value,
}) {
if (isFirst) {
list![index].segment.first = value;
} else {
list![index].segment.second = value;
}
if (list![index].category == SegmentType.poi_highlight ||
list![index].actionType == ActionType.full) {
list![index].segment.second = value;
}
}
List<Widget> segmentWidget({
required int index,
required bool isFirst,
}) {
String value = Utils.timeFormat(
isFirst ? list![index].segment.first : list![index].segment.second);
return [
Text(
'${isFirst ? '开始' : '结束'}: $value',
),
const SizedBox(width: 5),
iconButton(
context: context,
size: 26,
tooltip: '使用当前位置时间',
icon: Icons.my_location,
onPressed: () {
setState(() {
updateSegment(
isFirst: isFirst,
index: index,
value: plPlayerController.positionSeconds.value,
);
});
},
),
const SizedBox(width: 5),
iconButton(
context: context,
size: 26,
tooltip: '编辑',
icon: Icons.edit,
onPressed: () {
showDialog(
context: context,
builder: (context) {
String initV = value;
return AlertDialog(
content: TextFormField(
initialValue: value,
autofocus: true,
onChanged: (value) {
initV = value;
},
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'[\d:]+'),
),
],
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline),
),
),
TextButton(
onPressed: () => Get.back(result: initV),
child: Text('确定'),
),
],
);
},
).then((res) {
if (res != null) {
try {
List<int> split = (res as String)
.split(':')
.toList()
.reversed
.toList()
.map((e) => int.parse(e))
.toList();
int duration = 0;
for (int i = 0; i < split.length; i++) {
duration += split[i] * pow(60, i).toInt();
}
if (duration <=
plPlayerController.durationSeconds.value.inSeconds) {
setState(() {
updateSegment(
isFirst: isFirst,
index: index,
value: duration,
);
});
}
} catch (e) {
debugPrint(e.toString());
}
}
});
},
),
];
}
}

View File

@@ -3,12 +3,12 @@ import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart'; import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/video/reply/item.dart'; import 'package:PiliPlus/models/video/reply/item.dart';
import 'package:PiliPlus/pages/common/common_slide_page.dart';
import 'package:PiliPlus/pages/video/detail/reply/widgets/reply_item.dart'; import 'package:PiliPlus/pages/video/detail/reply/widgets/reply_item.dart';
import 'package:PiliPlus/pages/video/detail/reply/widgets/reply_item_grpc.dart'; import 'package:PiliPlus/pages/video/detail/reply/widgets/reply_item_grpc.dart';
import 'package:PiliPlus/pages/video/detail/reply_new/reply_page.dart'; import 'package:PiliPlus/pages/video/detail/reply_new/reply_page.dart';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/global_data.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -19,7 +19,7 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'controller.dart'; import 'controller.dart';
class VideoReplyReplyPanel extends StatefulWidget { class VideoReplyReplyPanel extends CommonSlidePage {
const VideoReplyReplyPanel({ const VideoReplyReplyPanel({
super.key, super.key,
this.id, this.id,
@@ -52,7 +52,8 @@ class VideoReplyReplyPanel extends StatefulWidget {
State<VideoReplyReplyPanel> createState() => _VideoReplyReplyPanelState(); State<VideoReplyReplyPanel> createState() => _VideoReplyReplyPanelState();
} }
class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> class _VideoReplyReplyPanelState
extends CommonSlidePageState<VideoReplyReplyPanel>
with TickerProviderStateMixin { with TickerProviderStateMixin {
late VideoReplyReplyController _videoReplyReplyController; late VideoReplyReplyController _videoReplyReplyController;
late final _savedReplies = {}; late final _savedReplies = {};
@@ -116,21 +117,8 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel>
}, },
); );
Offset? _downPos;
bool? _isSliding;
late final Rx<double> padding = 0.0.obs;
@override @override
Widget build(BuildContext context) { Widget get buildPage => Scaffold(
return GStorage.slideDismissReplyPage
? Padding(
padding: EdgeInsets.only(top: padding.value),
child: _buildPage,
)
: _buildPage;
}
Widget get _buildPage => Scaffold(
key: _key, key: _key,
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
body: Column( body: Column(
@@ -165,73 +153,14 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel>
color: Theme.of(context).dividerColor.withOpacity(0.1), color: Theme.of(context).dividerColor.withOpacity(0.1),
), ),
Expanded( Expanded(
child: GStorage.slideDismissReplyPage child: enableSlide ? slideList() : buildList,
? GestureDetector(
onPanDown: (event) {
if (event.localPosition.dx > 30) {
_isSliding = false;
} else {
_downPos = event.localPosition;
}
},
onPanUpdate: (event) {
if (_isSliding == false) {
return;
} else if (_isSliding == null) {
if (_downPos != null) {
Offset cumulativeDelta =
event.localPosition - _downPos!;
if (cumulativeDelta.dx.abs() >=
cumulativeDelta.dy.abs()) {
_isSliding = true;
setState(() {
padding.value = event.localPosition.dx;
});
} else {
_isSliding = false;
}
}
} else if (_isSliding == true) {
setState(() {
padding.value = event.localPosition.dx;
});
}
},
onPanCancel: () {
if (_isSliding == true) {
if (padding.value >= 100) {
Get.back();
} else {
setState(() {
padding.value = 0;
});
}
}
_downPos = null;
_isSliding = null;
},
onPanEnd: (event) {
if (_isSliding == true) {
if (padding.value >= 100) {
Get.back();
} else {
setState(() {
padding.value = 0;
});
}
}
_downPos = null;
_isSliding = null;
},
child: _buildList,
)
: _buildList,
), ),
], ],
), ),
); );
Widget get _buildList => ClipRect( @override
Widget get buildList => ClipRect(
child: refreshIndicator( child: refreshIndicator(
onRefresh: () async { onRefresh: () async {
await _videoReplyReplyController.onRefresh(); await _videoReplyReplyController.onRefresh();

View File

@@ -3,9 +3,7 @@ import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/icon_button.dart';
import 'package:PiliPlus/common/widgets/list_sheet.dart'; import 'package:PiliPlus/common/widgets/list_sheet.dart';
import 'package:PiliPlus/common/widgets/segment_progress_bar.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/bangumi/info.dart'; import 'package:PiliPlus/models/bangumi/info.dart';
import 'package:PiliPlus/models/common/reply_type.dart'; import 'package:PiliPlus/models/common/reply_type.dart';
@@ -17,6 +15,7 @@ import 'package:PiliPlus/pages/video/detail/introduction/widgets/page.dart';
import 'package:PiliPlus/pages/video/detail/introduction/widgets/season.dart'; import 'package:PiliPlus/pages/video/detail/introduction/widgets/season.dart';
import 'package:PiliPlus/pages/video/detail/member/horizontal_member_page.dart'; import 'package:PiliPlus/pages/video/detail/member/horizontal_member_page.dart';
import 'package:PiliPlus/pages/video/detail/reply_reply/view.dart'; import 'package:PiliPlus/pages/video/detail/reply_reply/view.dart';
import 'package:PiliPlus/pages/video/detail/view_point/view_points_page.dart';
import 'package:PiliPlus/pages/video/detail/widgets/ai_detail.dart'; import 'package:PiliPlus/pages/video/detail/widgets/ai_detail.dart';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/global_data.dart';
@@ -1105,7 +1104,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
} }
break; break;
case 'note': case 'note':
videoDetailController.showNoteList(); videoDetailController.showNoteList(context);
break; break;
} }
}, },
@@ -1546,7 +1545,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
VideoIntroPanel( VideoIntroPanel(
heroTag: heroTag, heroTag: heroTag,
showAiBottomSheet: showAiBottomSheet, showAiBottomSheet: showAiBottomSheet,
showIntroDetail: showIntroDetail,
showEpisodes: showEpisodes, showEpisodes: showEpisodes,
onShowMemberPage: onShowMemberPage, onShowMemberPage: onShowMemberPage,
), ),
@@ -1806,15 +1804,14 @@ class _VideoDetailPageState extends State<VideoDetailPage>
// ai总结 // ai总结
showAiBottomSheet() { showAiBottomSheet() {
videoDetailController.childKey.currentState?.showBottomSheet( videoDetailController.childKey.currentState?.showBottomSheet(
enableDrag: true, backgroundColor: Colors.transparent,
backgroundColor: Theme.of(context).colorScheme.surface,
(context) => AiDetail(modelResult: videoIntroController.modelResult), (context) => AiDetail(modelResult: videoIntroController.modelResult),
); );
} }
showIntroDetail(videoDetail, videoTags) { showIntroDetail(videoDetail, videoTags) {
videoDetailController.childKey.currentState?.showBottomSheet( videoDetailController.childKey.currentState?.showBottomSheet(
enableDrag: true, shape: const RoundedRectangleBorder(),
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
(context) => videoDetail is BangumiInfoModel (context) => videoDetail is BangumiInfoModel
? bangumi.IntroDetail( ? bangumi.IntroDetail(
@@ -1829,7 +1826,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
} }
showEpisodes(index, season, episodes, bvid, aid, cid) { showEpisodes(index, season, episodes, bvid, aid, cid) {
Widget listSheetContent() => ListSheetContent( Widget listSheetContent([bool? enableSlide]) => ListSheetContent(
enableSlide: enableSlide,
index: index, index: index,
season: season, season: season,
bvid: bvid, bvid: bvid,
@@ -1860,15 +1858,12 @@ class _VideoDetailPageState extends State<VideoDetailPage>
); );
if (isFullScreen) { if (isFullScreen) {
Utils.showFSSheet( Utils.showFSSheet(
child: Material( child: listSheetContent(false),
color: Theme.of(context).colorScheme.surface, isFullScreen: () => isFullScreen,
child: listSheetContent(),
),
isFullScreen: isFullScreen,
); );
} else { } else {
videoDetailController.childKey.currentState?.showBottomSheet( videoDetailController.childKey.currentState?.showBottomSheet(
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Colors.transparent,
(context) => listSheetContent(), (context) => listSheetContent(),
); );
} }
@@ -1945,150 +1940,22 @@ class _VideoDetailPageState extends State<VideoDetailPage>
} }
void showViewPoints() { void showViewPoints() {
Widget listSheetContent() {
int currentIndex = -1;
return StatefulBuilder(
builder: (context, setState) => Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
titleSpacing: 16,
title: const Text('分段信息'),
actions: [
Text(
'分段进度条',
style: TextStyle(fontSize: 16),
),
Obx(
() => Transform.scale(
alignment: Alignment.centerLeft,
scale: 0.8,
child: Switch(
thumbIcon: WidgetStateProperty.resolveWith<Icon?>((states) {
if (states.isNotEmpty &&
states.first == WidgetState.selected) {
return const Icon(Icons.done);
}
return null;
}),
value:
videoDetailController.plPlayerController.showVP.value,
onChanged: (value) {
videoDetailController.plPlayerController.showVP.value =
value;
},
),
),
),
iconButton(
context: context,
size: 30,
icon: Icons.clear,
tooltip: '关闭',
onPressed: Get.back,
),
const SizedBox(width: 16),
],
),
body: SingleChildScrollView(
child: Column(
children: [
...List.generate(
videoDetailController.viewPointList.length * 2 - 1,
(rawIndex) {
if (rawIndex % 2 == 1) {
return Divider(
height: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
);
}
int index = rawIndex ~/ 2;
Segment segment = videoDetailController.viewPointList[index];
if (currentIndex == -1 &&
segment.from != null &&
segment.to != null) {
if (videoDetailController
.plPlayerController.positionSeconds.value >=
segment.from! &&
videoDetailController
.plPlayerController.positionSeconds.value <
segment.to!) {
currentIndex = index;
}
}
return ListTile(
dense: true,
onTap: segment.from != null
? () {
currentIndex = index;
plPlayerController?.danmakuController?.clear();
plPlayerController?.videoPlayerController
?.seek(Duration(seconds: segment.from!));
Get.back();
}
: null,
leading: segment.url?.isNotEmpty == true
? Container(
margin: const EdgeInsets.symmetric(vertical: 6),
decoration: currentIndex == index
? BoxDecoration(
borderRadius: BorderRadius.circular(6),
border: Border.all(
width: 1.8,
strokeAlign:
BorderSide.strokeAlignOutside,
color:
Theme.of(context).colorScheme.primary,
),
)
: null,
child: LayoutBuilder(
builder: (context, constraints) =>
NetworkImgLayer(
radius: 6,
src: segment.url,
width: constraints.maxHeight *
StyleString.aspectRatio,
height: constraints.maxHeight,
),
),
)
: null,
title: Text(
segment.title ?? '',
style: TextStyle(
fontSize: 14,
fontWeight:
currentIndex == index ? FontWeight.bold : null,
color: currentIndex == index
? Theme.of(context).colorScheme.primary
: null,
),
),
subtitle: Text(
'${segment.from != null ? Utils.timeFormat(segment.from) : ''} - ${segment.to != null ? Utils.timeFormat(segment.to) : ''}',
style: TextStyle(
fontSize: 13,
color: currentIndex == index
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
),
),
);
}),
SizedBox(height: 25 + MediaQuery.paddingOf(context).bottom),
],
),
),
),
);
}
if (isFullScreen) { if (isFullScreen) {
Utils.showFSSheet(child: listSheetContent(), isFullScreen: isFullScreen); Utils.showFSSheet(
child: ViewPointsPage(
enableSlide: false,
videoDetailController: videoDetailController,
plPlayerController: plPlayerController,
),
isFullScreen: () => isFullScreen,
);
} else { } else {
videoDetailController.childKey.currentState?.showBottomSheet( videoDetailController.childKey.currentState?.showBottomSheet(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
(context) => listSheetContent(), (context) => ViewPointsPage(
videoDetailController: videoDetailController,
plPlayerController: plPlayerController,
),
); );
} }
} }
@@ -2109,6 +1976,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
void onShowMemberPage(mid) { void onShowMemberPage(mid) {
videoDetailController.childKey.currentState?.showBottomSheet( videoDetailController.childKey.currentState?.showBottomSheet(
shape: const RoundedRectangleBorder(),
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
(context) { (context) {
return HorizontalMemberPage( return HorizontalMemberPage(
@@ -2117,7 +1985,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
videoIntroController: videoIntroController, videoIntroController: videoIntroController,
); );
}, },
enableDrag: true,
); );
} }
} }

View File

@@ -0,0 +1,199 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/icon_button.dart';
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/segment_progress_bar.dart';
import 'package:PiliPlus/pages/common/common_slide_page.dart';
import 'package:PiliPlus/pages/video/detail/index.dart';
import 'package:PiliPlus/plugin/pl_player/index.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class ViewPointsPage extends CommonSlidePage {
const ViewPointsPage({
super.key,
super.enableSlide,
required this.videoDetailController,
required this.plPlayerController,
});
final VideoDetailController videoDetailController;
final PlPlayerController? plPlayerController;
@override
State<ViewPointsPage> createState() => _ViewPointsPageState();
}
class _ViewPointsPageState extends CommonSlidePageState<ViewPointsPage> {
late bool _isInit = true;
VideoDetailController get videoDetailController =>
widget.videoDetailController;
PlPlayerController? get plPlayerController => widget.plPlayerController;
@override
void initState() {
super.initState();
if (enableSlide && GStorage.collapsibleVideoPage) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_isInit = false;
});
}
});
}
}
int currentIndex = -1;
@override
Widget build(BuildContext context) {
if (enableSlide && GStorage.collapsibleVideoPage && _isInit) {
return CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
);
}
return enableSlide
? Padding(
padding: EdgeInsets.only(top: padding),
child: buildPage,
)
: buildPage;
}
@override
Widget get buildPage => Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
automaticallyImplyLeading: false,
titleSpacing: 16,
title: const Text('分段信息'),
actions: [
Text(
'分段进度条',
style: TextStyle(fontSize: 16),
),
Obx(
() => Transform.scale(
alignment: Alignment.centerLeft,
scale: 0.8,
child: Switch(
thumbIcon: WidgetStateProperty.resolveWith<Icon?>((states) {
if (states.isNotEmpty &&
states.first == WidgetState.selected) {
return const Icon(Icons.done);
}
return null;
}),
value: videoDetailController.plPlayerController.showVP.value,
onChanged: (value) {
videoDetailController.plPlayerController.showVP.value =
value;
},
),
),
),
iconButton(
context: context,
size: 30,
icon: Icons.clear,
tooltip: '关闭',
onPressed: Get.back,
),
const SizedBox(width: 16),
],
),
body: enableSlide ? slideList() : buildList,
);
@override
Widget get buildList => SingleChildScrollView(
controller: ScrollController(),
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
children: [
...List.generate(videoDetailController.viewPointList.length * 2 - 1,
(rawIndex) {
if (rawIndex % 2 == 1) {
return Divider(
height: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
);
}
int index = rawIndex ~/ 2;
Segment segment = videoDetailController.viewPointList[index];
if (currentIndex == -1 &&
segment.from != null &&
segment.to != null) {
if (videoDetailController
.plPlayerController.positionSeconds.value >=
segment.from! &&
videoDetailController
.plPlayerController.positionSeconds.value <
segment.to!) {
currentIndex = index;
}
}
return ListTile(
dense: true,
onTap: segment.from != null
? () {
currentIndex = index;
plPlayerController?.danmakuController?.clear();
plPlayerController?.videoPlayerController
?.seek(Duration(seconds: segment.from!));
Get.back();
}
: null,
leading: segment.url?.isNotEmpty == true
? Container(
margin: const EdgeInsets.symmetric(vertical: 6),
decoration: currentIndex == index
? BoxDecoration(
borderRadius: BorderRadius.circular(6),
border: Border.all(
width: 1.8,
strokeAlign: BorderSide.strokeAlignOutside,
color: Theme.of(context).colorScheme.primary,
),
)
: null,
child: LayoutBuilder(
builder: (context, constraints) => NetworkImgLayer(
radius: 6,
src: segment.url,
width:
constraints.maxHeight * StyleString.aspectRatio,
height: constraints.maxHeight,
),
),
)
: null,
title: Text(
segment.title ?? '',
style: TextStyle(
fontSize: 14,
fontWeight: currentIndex == index ? FontWeight.bold : null,
color: currentIndex == index
? Theme.of(context).colorScheme.primary
: null,
),
),
subtitle: Text(
'${segment.from != null ? Utils.timeFormat(segment.from) : ''} - ${segment.to != null ? Utils.timeFormat(segment.to) : ''}',
style: TextStyle(
fontSize: 13,
color: currentIndex == index
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
),
),
);
}),
SizedBox(height: 25 + MediaQuery.paddingOf(context).bottom),
],
),
);
}

View File

@@ -4,9 +4,7 @@ import 'dart:math';
import 'dart:ui'; import 'dart:ui';
import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/icon_button.dart';
import 'package:PiliPlus/common/widgets/list_sheet.dart'; import 'package:PiliPlus/common/widgets/list_sheet.dart';
import 'package:PiliPlus/common/widgets/segment_progress_bar.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/bangumi/info.dart'; import 'package:PiliPlus/models/bangumi/info.dart';
import 'package:PiliPlus/models/common/reply_type.dart'; import 'package:PiliPlus/models/common/reply_type.dart';
@@ -18,6 +16,7 @@ import 'package:PiliPlus/pages/video/detail/introduction/widgets/page.dart';
import 'package:PiliPlus/pages/video/detail/introduction/widgets/season.dart'; import 'package:PiliPlus/pages/video/detail/introduction/widgets/season.dart';
import 'package:PiliPlus/pages/video/detail/member/horizontal_member_page.dart'; import 'package:PiliPlus/pages/video/detail/member/horizontal_member_page.dart';
import 'package:PiliPlus/pages/video/detail/reply_reply/view.dart'; import 'package:PiliPlus/pages/video/detail/reply_reply/view.dart';
import 'package:PiliPlus/pages/video/detail/view_point/view_points_page.dart';
import 'package:PiliPlus/pages/video/detail/widgets/ai_detail.dart'; import 'package:PiliPlus/pages/video/detail/widgets/ai_detail.dart';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/global_data.dart';
@@ -35,7 +34,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/http/user.dart';
import 'package:PiliPlus/models/common/search_type.dart'; import 'package:PiliPlus/models/common/search_type.dart';
import 'package:PiliPlus/pages/bangumi/introduction/index.dart'; import 'package:PiliPlus/pages/bangumi/introduction/index.dart';
@@ -1398,7 +1396,7 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
} }
break; break;
case 'note': case 'note':
videoDetailController.showNoteList(); videoDetailController.showNoteList(context);
break; break;
} }
}, },
@@ -1862,7 +1860,6 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
VideoIntroPanel( VideoIntroPanel(
heroTag: heroTag, heroTag: heroTag,
showAiBottomSheet: showAiBottomSheet, showAiBottomSheet: showAiBottomSheet,
showIntroDetail: showIntroDetail,
showEpisodes: showEpisodes, showEpisodes: showEpisodes,
onShowMemberPage: onShowMemberPage, onShowMemberPage: onShowMemberPage,
), ),
@@ -2126,15 +2123,14 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
// ai总结 // ai总结
showAiBottomSheet() { showAiBottomSheet() {
videoDetailController.childKey.currentState?.showBottomSheet( videoDetailController.childKey.currentState?.showBottomSheet(
enableDrag: true, backgroundColor: Colors.transparent,
backgroundColor: Theme.of(context).colorScheme.surface,
(context) => AiDetail(modelResult: videoIntroController.modelResult), (context) => AiDetail(modelResult: videoIntroController.modelResult),
); );
} }
showIntroDetail(videoDetail, videoTags) { showIntroDetail(videoDetail, videoTags) {
videoDetailController.childKey.currentState?.showBottomSheet( videoDetailController.childKey.currentState?.showBottomSheet(
enableDrag: true, shape: const RoundedRectangleBorder(),
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
(context) => videoDetail is BangumiInfoModel (context) => videoDetail is BangumiInfoModel
? bangumi.IntroDetail( ? bangumi.IntroDetail(
@@ -2149,7 +2145,8 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
} }
showEpisodes(index, season, episodes, bvid, aid, cid) { showEpisodes(index, season, episodes, bvid, aid, cid) {
Widget listSheetContent() => ListSheetContent( Widget listSheetContent([bool? enableSlide]) => ListSheetContent(
enableSlide: enableSlide,
index: index, index: index,
season: season, season: season,
bvid: bvid, bvid: bvid,
@@ -2180,15 +2177,12 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
); );
if (isFullScreen) { if (isFullScreen) {
Utils.showFSSheet( Utils.showFSSheet(
child: Material( child: listSheetContent(false),
color: Theme.of(context).colorScheme.surface, isFullScreen: () => isFullScreen,
child: listSheetContent(),
),
isFullScreen: isFullScreen,
); );
} else { } else {
videoDetailController.childKey.currentState?.showBottomSheet( videoDetailController.childKey.currentState?.showBottomSheet(
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Colors.transparent,
(context) => listSheetContent(), (context) => listSheetContent(),
); );
} }
@@ -2265,155 +2259,22 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
} }
void showViewPoints() { void showViewPoints() {
Widget listSheetContent() {
int currentIndex = -1;
return StatefulBuilder(
builder: (context, setState) => Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
automaticallyImplyLeading: false,
titleSpacing: 16,
title: const Text('分段信息'),
actions: [
Text(
'分段进度条',
style: TextStyle(fontSize: 16),
),
Obx(
() => Transform.scale(
alignment: Alignment.centerLeft,
scale: 0.8,
child: Switch(
thumbIcon: WidgetStateProperty.resolveWith<Icon?>((states) {
if (states.isNotEmpty &&
states.first == WidgetState.selected) {
return const Icon(Icons.done);
}
return null;
}),
value:
videoDetailController.plPlayerController.showVP.value,
onChanged: (value) {
videoDetailController.plPlayerController.showVP.value =
value;
},
),
),
),
iconButton(
context: context,
size: 30,
icon: Icons.clear,
tooltip: '关闭',
onPressed: Get.back,
),
const SizedBox(width: 16),
],
),
body: SingleChildScrollView(
controller: ScrollController(),
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
children: [
...List.generate(
videoDetailController.viewPointList.length * 2 - 1,
(rawIndex) {
if (rawIndex % 2 == 1) {
return Divider(
height: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
);
}
int index = rawIndex ~/ 2;
Segment segment = videoDetailController.viewPointList[index];
if (currentIndex == -1 &&
segment.from != null &&
segment.to != null) {
if (videoDetailController
.plPlayerController.positionSeconds.value >=
segment.from! &&
videoDetailController
.plPlayerController.positionSeconds.value <
segment.to!) {
currentIndex = index;
}
}
return ListTile(
dense: true,
onTap: segment.from != null
? () {
currentIndex = index;
plPlayerController?.danmakuController?.clear();
plPlayerController?.videoPlayerController
?.seek(Duration(seconds: segment.from!));
Get.back();
}
: null,
leading: segment.url?.isNotEmpty == true
? Container(
margin: const EdgeInsets.symmetric(vertical: 6),
decoration: currentIndex == index
? BoxDecoration(
borderRadius: BorderRadius.circular(6),
border: Border.all(
width: 1.8,
strokeAlign:
BorderSide.strokeAlignOutside,
color:
Theme.of(context).colorScheme.primary,
),
)
: null,
child: LayoutBuilder(
builder: (context, constraints) =>
NetworkImgLayer(
radius: 6,
src: segment.url,
width: constraints.maxHeight *
StyleString.aspectRatio,
height: constraints.maxHeight,
),
),
)
: null,
title: Text(
segment.title ?? '',
style: TextStyle(
fontSize: 14,
fontWeight:
currentIndex == index ? FontWeight.bold : null,
color: currentIndex == index
? Theme.of(context).colorScheme.primary
: null,
),
),
subtitle: Text(
'${segment.from != null ? Utils.timeFormat(segment.from) : ''} - ${segment.to != null ? Utils.timeFormat(segment.to) : ''}',
style: TextStyle(
fontSize: 13,
color: currentIndex == index
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
),
),
);
}),
SizedBox(height: 25 + MediaQuery.paddingOf(context).bottom),
],
),
),
),
);
}
if (isFullScreen) { if (isFullScreen) {
Utils.showFSSheet(child: listSheetContent(), isFullScreen: isFullScreen); Utils.showFSSheet(
child: ViewPointsPage(
enableSlide: false,
videoDetailController: videoDetailController,
plPlayerController: plPlayerController,
),
isFullScreen: () => isFullScreen,
);
} else { } else {
videoDetailController.childKey.currentState?.showBottomSheet( videoDetailController.childKey.currentState?.showBottomSheet(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
(context) => GStorage.collapsibleVideoPage (context) => ViewPointsPage(
? ViewPointsPage(child: listSheetContent()) videoDetailController: videoDetailController,
: listSheetContent(), plPlayerController: plPlayerController,
),
); );
} }
} }
@@ -2434,6 +2295,7 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
void onShowMemberPage(mid) { void onShowMemberPage(mid) {
videoDetailController.childKey.currentState?.showBottomSheet( videoDetailController.childKey.currentState?.showBottomSheet(
shape: const RoundedRectangleBorder(),
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
(context) { (context) {
return HorizontalMemberPage( return HorizontalMemberPage(
@@ -2442,41 +2304,6 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
videoIntroController: videoIntroController, videoIntroController: videoIntroController,
); );
}, },
enableDrag: true,
); );
} }
} }
class ViewPointsPage extends StatefulWidget {
const ViewPointsPage({super.key, required this.child});
final Widget child;
@override
State<ViewPointsPage> createState() => _ViewPointsPageState();
}
class _ViewPointsPageState extends State<ViewPointsPage> {
bool _isInit = true;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_isInit = false;
});
}
});
}
@override
Widget build(BuildContext context) {
return _isInit
? CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
)
: widget.child;
}
}

View File

@@ -1,13 +1,14 @@
import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/pages/common/common_slide_page.dart';
import 'package:PiliPlus/pages/video/detail/controller.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:PiliPlus/models/video/ai.dart'; import 'package:PiliPlus/models/video/ai.dart';
import 'package:PiliPlus/pages/video/detail/index.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
class AiDetail extends StatelessWidget { class AiDetail extends CommonSlidePage {
final ModelResult modelResult; final ModelResult modelResult;
const AiDetail({ const AiDetail({
@@ -16,157 +17,10 @@ class AiDetail extends StatelessWidget {
}); });
@override @override
Widget build(BuildContext context) { State<AiDetail> createState() => _AiDetailState();
return Container( }
color: Theme.of(context).colorScheme.surface,
padding: const EdgeInsets.symmetric(horizontal: 14),
// height: Utils.getSheetHeight(context),
child: Column(
children: [
InkWell(
onTap: Get.back,
child: Container(
height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: const BorderRadius.all(Radius.circular(3)),
),
),
),
),
),
Expanded(
child: SingleChildScrollView(
controller: ScrollController(),
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
children: [
if (modelResult.summary?.isNotEmpty == true) ...[
SelectableText(
'总结: ${modelResult.summary}',
style: const TextStyle(
fontSize: 15,
height: 1.5,
),
),
if (modelResult.outline?.isNotEmpty == true)
Divider(
height: 20,
color: Theme.of(context).dividerColor.withOpacity(0.1),
thickness: 6,
)
],
if (modelResult.outline?.isNotEmpty == true)
ListView.builder(
shrinkWrap: true,
itemCount: modelResult.outline!.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return Column(
children: [
SelectableText(
modelResult.outline![index].title!,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
const SizedBox(height: 6),
if (modelResult
.outline![index].partOutline?.isNotEmpty ==
true)
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: modelResult
.outline![index].partOutline!.length,
itemBuilder: (context, i) {
return Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Wrap(
children: [
SelectableText.rich(
TextSpan(
style: TextStyle(
fontSize: 13,
color: Theme.of(context)
.colorScheme
.onSurface,
height: 1.5,
),
children: [
TextSpan(
text: Utils.tampToSeektime(
modelResult
.outline![index]
.partOutline![i]
.timestamp!),
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
),
recognizer:
TapGestureRecognizer()
..onTap = () {
// 跳转到指定位置
try {
Get.find<VideoDetailController>(
tag: Get.arguments[
'heroTag'])
.plPlayerController
.seekTo(
Duration(
seconds: Utils
.duration(
Utils.tampToSeektime(modelResult
.outline![index]
.partOutline![i]
.timestamp!)
.toString(),
),
),
);
} catch (_) {}
},
),
const TextSpan(text: ' '),
TextSpan(
text: modelResult
.outline![index]
.partOutline![i]
.content!),
],
),
),
],
),
],
);
},
),
const SizedBox(height: 20),
],
);
},
)
],
),
),
),
],
),
);
}
class _AiDetailState extends CommonSlidePageState<AiDetail> {
InlineSpan buildContent(BuildContext context, content) { InlineSpan buildContent(BuildContext context, content) {
List descV2 = content.descV2; List descV2 = content.descV2;
// type // type
@@ -233,4 +87,158 @@ class AiDetail extends StatelessWidget {
}); });
return TextSpan(children: spanChildren); return TextSpan(children: spanChildren);
} }
@override
Widget get buildPage => Container(
color: Theme.of(context).colorScheme.surface,
padding: const EdgeInsets.symmetric(horizontal: 14),
child: Column(
children: [
InkWell(
onTap: Get.back,
child: Container(
height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: const BorderRadius.all(Radius.circular(3)),
),
),
),
),
),
Expanded(
child: enableSlide ? slideList() : buildList,
),
],
),
);
@override
Widget get buildList => SingleChildScrollView(
controller: ScrollController(),
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
children: [
if (widget.modelResult.summary?.isNotEmpty == true) ...[
SelectableText(
'总结: ${widget.modelResult.summary}',
style: const TextStyle(
fontSize: 15,
height: 1.5,
),
),
if (widget.modelResult.outline?.isNotEmpty == true)
Divider(
height: 20,
color: Theme.of(context).dividerColor.withOpacity(0.1),
thickness: 6,
)
],
if (widget.modelResult.outline?.isNotEmpty == true)
ListView.builder(
shrinkWrap: true,
itemCount: widget.modelResult.outline!.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return Column(
children: [
SelectableText(
widget.modelResult.outline![index].title!,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
const SizedBox(height: 6),
if (widget.modelResult.outline![index].partOutline
?.isNotEmpty ==
true)
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: widget
.modelResult.outline![index].partOutline!.length,
itemBuilder: (context, i) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
children: [
SelectableText.rich(
TextSpan(
style: TextStyle(
fontSize: 13,
color: Theme.of(context)
.colorScheme
.onSurface,
height: 1.5,
),
children: [
TextSpan(
text: Utils.tampToSeektime(widget
.modelResult
.outline![index]
.partOutline![i]
.timestamp!),
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
// 跳转到指定位置
try {
Get.find<VideoDetailController>(
tag: Get.arguments[
'heroTag'])
.plPlayerController
.seekTo(
Duration(
seconds:
Utils.duration(
Utils.tampToSeektime(widget
.modelResult
.outline![
index]
.partOutline![
i]
.timestamp!)
.toString(),
),
),
);
} catch (_) {}
},
),
const TextSpan(text: ' '),
TextSpan(
text: widget
.modelResult
.outline![index]
.partOutline![i]
.content!),
],
),
),
],
),
],
);
},
),
const SizedBox(height: 20),
],
);
},
)
],
),
);
} }

View File

@@ -100,7 +100,7 @@ class _HeaderControlState extends State<HeaderControl> {
/// 设置面板 /// 设置面板
void showSettingSheet() { void showSettingSheet() {
Utils.showFSSheet( Utils.showFSSheet(
isFullScreen: isFullScreen, isFullScreen: () => isFullScreen,
child: Container( child: Container(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -162,7 +162,7 @@ class _HeaderControlState extends State<HeaderControl> {
dense: true, dense: true,
onTap: () { onTap: () {
Get.back(); Get.back();
widget.videoDetailCtr.showNoteList(); widget.videoDetailCtr.showNoteList(context);
}, },
leading: const Icon(Icons.note_alt_outlined, size: 20), leading: const Icon(Icons.note_alt_outlined, size: 20),
title: const Text('查看笔记', style: titleStyle), title: const Text('查看笔记', style: titleStyle),
@@ -570,7 +570,7 @@ class _HeaderControlState extends State<HeaderControl> {
void scheduleExit() async { void scheduleExit() async {
const List<int> scheduleTimeChoices = [0, 15, 30, 45, 60]; const List<int> scheduleTimeChoices = [0, 15, 30, 45, 60];
Utils.showFSSheet( Utils.showFSSheet(
isFullScreen: isFullScreen, isFullScreen: () => isFullScreen,
child: StatefulBuilder( child: StatefulBuilder(
builder: (context, setState) { builder: (context, setState) {
return Container( return Container(
@@ -778,7 +778,7 @@ class _HeaderControlState extends State<HeaderControl> {
} }
Utils.showFSSheet( Utils.showFSSheet(
isFullScreen: isFullScreen, isFullScreen: () => isFullScreen,
child: Container( child: Container(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -882,7 +882,7 @@ class _HeaderControlState extends State<HeaderControl> {
final AudioQuality currentAudioQa = widget.videoDetailCtr.currentAudioQa!; final AudioQuality currentAudioQa = widget.videoDetailCtr.currentAudioQa!;
final List<AudioItem> audio = videoInfo.dash!.audio!; final List<AudioItem> audio = videoInfo.dash!.audio!;
Utils.showFSSheet( Utils.showFSSheet(
isFullScreen: isFullScreen, isFullScreen: () => isFullScreen,
child: Container( child: Container(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -973,7 +973,7 @@ class _HeaderControlState extends State<HeaderControl> {
} }
Utils.showFSSheet( Utils.showFSSheet(
isFullScreen: isFullScreen, isFullScreen: () => isFullScreen,
child: Container( child: Container(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -1082,7 +1082,7 @@ class _HeaderControlState extends State<HeaderControl> {
final DanmakuController? danmakuController = final DanmakuController? danmakuController =
widget.controller.danmakuController; widget.controller.danmakuController;
Utils.showFSSheet( Utils.showFSSheet(
isFullScreen: isFullScreen, isFullScreen: () => isFullScreen,
padding: isFullScreen ? 70 : null, padding: isFullScreen ? 70 : null,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
@@ -1702,7 +1702,7 @@ class _HeaderControlState extends State<HeaderControl> {
/// 播放顺序 /// 播放顺序
void showSetRepeat() async { void showSetRepeat() async {
Utils.showFSSheet( Utils.showFSSheet(
isFullScreen: isFullScreen, isFullScreen: () => isFullScreen,
child: Container( child: Container(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(

View File

@@ -2,6 +2,7 @@ import 'package:PiliPlus/common/widgets/icon_button.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/stat/danmu.dart'; import 'package:PiliPlus/common/widgets/stat/danmu.dart';
import 'package:PiliPlus/common/widgets/stat/view.dart'; import 'package:PiliPlus/common/widgets/stat/view.dart';
import 'package:PiliPlus/pages/common/common_slide_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -14,7 +15,7 @@ import 'package:PiliPlus/utils/utils.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
class MediaListPanel extends StatefulWidget { class MediaListPanel extends CommonSlidePage {
const MediaListPanel({ const MediaListPanel({
super.key, super.key,
required this.mediaList, required this.mediaList,
@@ -42,7 +43,7 @@ class MediaListPanel extends StatefulWidget {
State<MediaListPanel> createState() => _MediaListPanelState(); State<MediaListPanel> createState() => _MediaListPanelState();
} }
class _MediaListPanelState extends State<MediaListPanel> { class _MediaListPanelState extends CommonSlidePageState<MediaListPanel> {
final _scrollController = ItemScrollController(); final _scrollController = ItemScrollController();
late RxBool desc; late RxBool desc;
@@ -62,7 +63,7 @@ class _MediaListPanelState extends State<MediaListPanel> {
} }
@override @override
Widget build(BuildContext context) { Widget get buildPage {
return Column( return Column(
children: [ children: [
AppBar( AppBar(
@@ -92,19 +93,22 @@ class _MediaListPanelState extends State<MediaListPanel> {
], ],
), ),
Expanded( Expanded(
child: widget.loadPrevious != null child: enableSlide ? slideList() : buildList,
? refreshIndicator(
onRefresh: () async {
await widget.loadPrevious!();
},
child: _buildList,
)
: _buildList,
), ),
], ],
); );
} }
@override
Widget get buildList => widget.loadPrevious != null
? refreshIndicator(
onRefresh: () async {
await widget.loadPrevious!();
},
child: _buildList,
)
: _buildList;
Widget get _buildList => Obx( Widget get _buildList => Obx(
() => ScrollablePositionedList.builder( () => ScrollablePositionedList.builder(
itemScrollController: _scrollController, itemScrollController: _scrollController,

View File

@@ -202,7 +202,7 @@ class Utils {
static void showFSSheet({ static void showFSSheet({
required Widget child, required Widget child,
required bool isFullScreen, required Function isFullScreen,
double? padding, double? padding,
}) { }) {
Navigator.of(Get.context!).push( Navigator.of(Get.context!).push(
@@ -212,15 +212,28 @@ class Utils {
? Column( ? Column(
children: [ children: [
const Spacer(flex: 3), const Spacer(flex: 3),
Expanded(flex: 7, child: child), Expanded(
if (isFullScreen && padding != null) flex: 7,
child: MediaQuery.removePadding(
context: Get.context!,
removeTop: true,
child: child,
),
),
if (isFullScreen() && padding != null)
SizedBox(height: padding), SizedBox(height: padding),
], ],
) )
: Row( : Row(
children: [ children: [
const Spacer(), const Spacer(),
Expanded(child: child), Expanded(
child: MediaQuery.removePadding(
context: Get.context!,
removeLeft: true,
child: child,
),
),
], ],
); );
}, },