mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
mod: more slide dismiss pages
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -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 ? '正序播放' : '倒序播放',
|
||||||
|
|||||||
@@ -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 => [
|
||||||
|
|||||||
@@ -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)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
91
lib/pages/common/common_slide_page.dart
Normal file
91
lib/pages/common/common_slide_page.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
548
lib/pages/video/detail/post_panel/post_panel.dart
Normal file
548
lib/pages/video/detail/post_panel/post_panel.dart
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
199
lib/pages/video/detail/view_point/view_points_page.dart
Normal file
199
lib/pages/video/detail/view_point/view_points_page.dart
Normal 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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user