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/models/bangumi/info.dart' as bangumi;
|
||||
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_smart_dialog/flutter_smart_dialog.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 'package:PiliPlus/common/widgets/spring_physics.dart';
|
||||
|
||||
class ListSheetContent extends StatefulWidget {
|
||||
class ListSheetContent extends CommonSlidePage {
|
||||
const ListSheetContent({
|
||||
super.key,
|
||||
this.index, // tab index
|
||||
@@ -31,6 +32,7 @@ class ListSheetContent extends StatefulWidget {
|
||||
this.showTitle,
|
||||
this.isSupportReverse,
|
||||
this.isReversed,
|
||||
super.enableSlide,
|
||||
});
|
||||
|
||||
final dynamic index;
|
||||
@@ -50,7 +52,7 @@ class ListSheetContent extends StatefulWidget {
|
||||
State<ListSheetContent> createState() => _ListSheetContentState();
|
||||
}
|
||||
|
||||
class _ListSheetContentState extends State<ListSheetContent>
|
||||
class _ListSheetContentState extends CommonSlidePageState<ListSheetContent>
|
||||
with TickerProviderStateMixin {
|
||||
late List<ItemScrollController> itemScrollController = [];
|
||||
late int currentIndex = _currentIndex;
|
||||
@@ -140,7 +142,7 @@ class _ListSheetContentState extends State<ListSheetContent>
|
||||
}();
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (GStorage.collapsibleVideoPage) {
|
||||
if (enableSlide && GStorage.collapsibleVideoPage) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isInit = false;
|
||||
@@ -293,176 +295,195 @@ class _ListSheetContentState extends State<ListSheetContent>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (GStorage.collapsibleVideoPage && _isInit) {
|
||||
if (enableSlide && GStorage.collapsibleVideoPage && _isInit) {
|
||||
return CustomScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 45,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: widget.showTitle != false ? 14 : 6),
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.showTitle != false)
|
||||
Text(
|
||||
'合集(${_isList ? widget.season.epCount : episodes?.length ?? ''})',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
StreamBuilder(
|
||||
stream: _favStream?.stream,
|
||||
builder: (context, snapshot) => snapshot.hasData
|
||||
? mediumButton(
|
||||
tooltip: _seasonFav == 1 ? '取消订阅' : '订阅',
|
||||
icon: _seasonFav == 1
|
||||
? 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']);
|
||||
}
|
||||
return enableSlide
|
||||
? Padding(
|
||||
padding: EdgeInsets.only(top: padding),
|
||||
child: buildPage,
|
||||
)
|
||||
: buildPage;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget get buildPage => Material(
|
||||
color: widget.showTitle == false
|
||||
? Colors.transparent
|
||||
: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 45,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: widget.showTitle != false ? 14 : 6),
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.showTitle != false)
|
||||
Text(
|
||||
'合集(${_isList ? widget.season.epCount : episodes?.length ?? ''})',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
StreamBuilder(
|
||||
stream: _favStream?.stream,
|
||||
builder: (context, snapshot) => snapshot.hasData
|
||||
? mediumButton(
|
||||
tooltip: _seasonFav == 1 ? '取消订阅' : '订阅',
|
||||
icon: _seasonFav == 1
|
||||
? 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(),
|
||||
),
|
||||
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
|
||||
),
|
||||
const Spacer(),
|
||||
StreamBuilder(
|
||||
stream: _indexStream?.stream,
|
||||
initialData: _index,
|
||||
builder: (context, snapshot) {
|
||||
return snapshot.data == _index
|
||||
? _reverseButton
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
StreamBuilder(
|
||||
stream: _indexStream?.stream,
|
||||
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),
|
||||
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];
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
: Material(
|
||||
color: Colors.transparent,
|
||||
child: _buildBody(null, episodes),
|
||||
),
|
||||
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),
|
||||
),
|
||||
),
|
||||
)
|
||||
: enableSlide
|
||||
? slideList()
|
||||
: buildList,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@override
|
||||
Widget get buildList => Material(
|
||||
color: Colors.transparent,
|
||||
child: _buildBody(null, episodes),
|
||||
);
|
||||
|
||||
Widget get _reverseButton => mediumButton(
|
||||
tooltip: widget.isReversed == true ? '正序播放' : '倒序播放',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:PiliPlus/models/common/sponsor_block/action_type.dart';
|
||||
|
||||
enum SegmentType {
|
||||
sponsor,
|
||||
selfpromo,
|
||||
@@ -13,6 +15,74 @@ enum SegmentType {
|
||||
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 {
|
||||
/// from https://github.com/hanydd/BilibiliSponsorBlock/blob/master/public/_locales/zh_CN/messages.json
|
||||
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:flutter/material.dart';
|
||||
import 'package:PiliPlus/common/widgets/stat/danmu.dart';
|
||||
@@ -6,7 +7,7 @@ import 'package:get/get.dart';
|
||||
|
||||
import '../../../../utils/utils.dart';
|
||||
|
||||
class IntroDetail extends StatelessWidget {
|
||||
class IntroDetail extends CommonSlidePage {
|
||||
final dynamic bangumiDetail;
|
||||
final dynamic videoTags;
|
||||
|
||||
@@ -17,11 +18,17 @@ class IntroDetail extends StatelessWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
TextStyle smallTitle = TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
);
|
||||
State<IntroDetail> createState() => _IntroDetailState();
|
||||
}
|
||||
|
||||
class _IntroDetailState extends CommonSlidePageState<IntroDetail> {
|
||||
late final TextStyle smallTitle = TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget get buildPage {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 14, right: 14),
|
||||
child: Column(
|
||||
@@ -46,99 +53,103 @@ class IntroDetail extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
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)
|
||||
],
|
||||
),
|
||||
),
|
||||
child: enableSlide ? slideList() : buildList,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@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 ValueChanged<String>? onSave;
|
||||
final bool autofocus;
|
||||
|
||||
@override
|
||||
State<CommonPublishPage> createState();
|
||||
}
|
||||
|
||||
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:math';
|
||||
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/segment_progress_bar.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/video/detail/introduction/controller.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/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/watch_later_list.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:floating/floating.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:PiliPlus/http/constants.dart';
|
||||
@@ -47,7 +43,6 @@ import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:PiliPlus/utils/video_utils.dart';
|
||||
import 'package:get/get_navigation/src/dialog/dialog_route.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
|
||||
|
||||
import '../../../utils/id_utils.dart';
|
||||
import 'widgets/header_control.dart';
|
||||
@@ -400,6 +395,7 @@ class VideoDetailController extends GetxController
|
||||
showMediaListPanel(context) {
|
||||
if (mediaList.isNotEmpty) {
|
||||
childKey.currentState?.showBottomSheet(
|
||||
shape: const RoundedRectangleBorder(),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
(context) => MediaListPanel(
|
||||
mediaList: mediaList,
|
||||
@@ -463,7 +459,7 @@ class VideoDetailController extends GetxController
|
||||
List<Color>? _blockColor;
|
||||
RxList<SegmentModel> segmentList = <SegmentModel>[].obs;
|
||||
List<Segment> viewPointList = <Segment>[];
|
||||
List<Segment>? _segmentProgressList;
|
||||
List<Segment>? segmentProgressList;
|
||||
Color _getColor(SegmentType segment) =>
|
||||
_blockColor?[segment.index] ?? segment.color;
|
||||
late RxString videoLabel = ''.obs;
|
||||
@@ -481,7 +477,7 @@ class VideoDetailController extends GetxController
|
||||
'userID': GStorage.blockUserID,
|
||||
'type': type,
|
||||
},
|
||||
options: _options,
|
||||
options: options,
|
||||
)
|
||||
.then((res) {
|
||||
SmartDialog.showToast(res.statusCode == 200 ? '投票成功' : '投票失败');
|
||||
@@ -510,7 +506,7 @@ class VideoDetailController extends GetxController
|
||||
'userID': GStorage.blockUserID,
|
||||
'category': item.name,
|
||||
},
|
||||
options: _options,
|
||||
options: options,
|
||||
)
|
||||
.then((res) {
|
||||
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 {
|
||||
positionSubscription?.cancel();
|
||||
videoLabel.value = '';
|
||||
segmentList.clear();
|
||||
_segmentProgressList = null;
|
||||
segmentProgressList = null;
|
||||
dynamic result = await Request().get(
|
||||
'${GStorage.blockServer}/api/skipSegments',
|
||||
queryParameters: {
|
||||
'videoID': bvid,
|
||||
'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) {
|
||||
try {
|
||||
List<String> list =
|
||||
@@ -767,8 +763,8 @@ class VideoDetailController extends GetxController
|
||||
).toList());
|
||||
|
||||
// _segmentProgressList
|
||||
_segmentProgressList ??= <Segment>[];
|
||||
_segmentProgressList!.addAll(segmentList.map((item) {
|
||||
segmentProgressList ??= <Segment>[];
|
||||
segmentProgressList!.addAll(segmentList.map((item) {
|
||||
double start = (item.segment.first / ((data.timeLength ?? 0) / 1000))
|
||||
.clamp(0.0, 1.0);
|
||||
double end = (item.segment.second / ((data.timeLength ?? 0) / 1000))
|
||||
@@ -789,7 +785,7 @@ class VideoDetailController extends GetxController
|
||||
: -1;
|
||||
}
|
||||
|
||||
void _initSkip() {
|
||||
void initSkip() {
|
||||
if (segmentList.isNotEmpty) {
|
||||
positionSubscription = plPlayerController
|
||||
.videoPlayerController?.stream.position
|
||||
@@ -920,7 +916,7 @@ class VideoDetailController extends GetxController
|
||||
Request().post(
|
||||
'${GStorage.blockServer}/api/viewedVideoSponsorTime',
|
||||
queryParameters: {'UUID': item.UUID},
|
||||
options: _options,
|
||||
options: options,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -1073,7 +1069,7 @@ class VideoDetailController extends GetxController
|
||||
'referer': HttpString.baseUrl
|
||||
},
|
||||
),
|
||||
segmentList: _segmentProgressList,
|
||||
segmentList: segmentProgressList,
|
||||
viewPointList: viewPointList,
|
||||
vttSubtitles: _vttSubtitles,
|
||||
vttSubtitlesIndex: vttSubtitlesIndex,
|
||||
@@ -1102,7 +1098,7 @@ class VideoDetailController extends GetxController
|
||||
},
|
||||
);
|
||||
|
||||
_initSkip();
|
||||
initSkip();
|
||||
|
||||
if (vttSubtitlesIndex == null) {
|
||||
_getSubtitle();
|
||||
@@ -1337,601 +1333,24 @@ class VideoDetailController extends GetxController
|
||||
}
|
||||
if (plPlayerController.isFullScreen.value) {
|
||||
Utils.showFSSheet(
|
||||
child: _postPanel(),
|
||||
isFullScreen: plPlayerController.isFullScreen.value,
|
||||
child: PostPanel(
|
||||
enableSlide: false,
|
||||
videoDetailController: this,
|
||||
plPlayerController: plPlayerController,
|
||||
),
|
||||
isFullScreen: () => plPlayerController.isFullScreen.value,
|
||||
);
|
||||
} else {
|
||||
childKey.currentState?.showBottomSheet(
|
||||
enableDrag: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
(context) => GStorage.collapsibleVideoPage
|
||||
? ViewPointsPage(child: _postPanel())
|
||||
: _postPanel(),
|
||||
(context) => PostPanel(
|
||||
videoDetailController: this,
|
||||
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>>[];
|
||||
int? vttSubtitlesIndex;
|
||||
late bool showVP = true;
|
||||
@@ -2139,7 +1558,7 @@ class VideoDetailController extends GetxController
|
||||
positionSubscription?.cancel();
|
||||
videoLabel.value = '';
|
||||
segmentList.clear();
|
||||
_segmentProgressList = null;
|
||||
segmentProgressList = null;
|
||||
}
|
||||
|
||||
// interactive video
|
||||
@@ -2177,11 +1596,14 @@ class VideoDetailController extends GetxController
|
||||
}
|
||||
}
|
||||
|
||||
void showNoteList() async {
|
||||
void showNoteList(BuildContext context) async {
|
||||
if (plPlayerController.isFullScreen.value) {
|
||||
Utils.showFSSheet(
|
||||
child: NoteListPage(oid: oid.value),
|
||||
isFullScreen: plPlayerController.isFullScreen.value,
|
||||
child: NoteListPage(
|
||||
oid: oid.value,
|
||||
enableSlide: false,
|
||||
),
|
||||
isFullScreen: () => plPlayerController.isFullScreen.value,
|
||||
);
|
||||
} else {
|
||||
childKey.currentState?.showBottomSheet(
|
||||
|
||||
@@ -35,13 +35,11 @@ class VideoIntroPanel extends StatefulWidget {
|
||||
super.key,
|
||||
required this.heroTag,
|
||||
required this.showAiBottomSheet,
|
||||
required this.showIntroDetail,
|
||||
required this.showEpisodes,
|
||||
required this.onShowMemberPage,
|
||||
});
|
||||
final String heroTag;
|
||||
final Function showAiBottomSheet;
|
||||
final Function showIntroDetail;
|
||||
final Function showEpisodes;
|
||||
final ValueChanged onShowMemberPage;
|
||||
|
||||
@@ -75,10 +73,6 @@ class _VideoIntroPanelState extends State<VideoIntroPanel>
|
||||
videoIntroController: videoIntroController,
|
||||
heroTag: widget.heroTag,
|
||||
showAiBottomSheet: widget.showAiBottomSheet,
|
||||
showIntroDetail: () => widget.showIntroDetail(
|
||||
videoIntroController.videoDetail.value,
|
||||
videoIntroController.videoTags,
|
||||
),
|
||||
showEpisodes: widget.showEpisodes,
|
||||
onShowMemberPage: widget.onShowMemberPage,
|
||||
)
|
||||
@@ -88,10 +82,6 @@ class _VideoIntroPanelState extends State<VideoIntroPanel>
|
||||
videoIntroController: videoIntroController,
|
||||
heroTag: widget.heroTag,
|
||||
showAiBottomSheet: widget.showAiBottomSheet,
|
||||
showIntroDetail: () => widget.showIntroDetail(
|
||||
videoIntroController.videoDetail.value,
|
||||
videoIntroController.videoTags,
|
||||
),
|
||||
showEpisodes: widget.showEpisodes,
|
||||
onShowMemberPage: widget.onShowMemberPage,
|
||||
),
|
||||
@@ -103,7 +93,6 @@ class VideoInfo extends StatefulWidget {
|
||||
final bool loadingStatus;
|
||||
final String heroTag;
|
||||
final Function showAiBottomSheet;
|
||||
final Function showIntroDetail;
|
||||
final Function showEpisodes;
|
||||
final ValueChanged onShowMemberPage;
|
||||
final VideoIntroController videoIntroController;
|
||||
@@ -113,7 +102,6 @@ class VideoInfo extends StatefulWidget {
|
||||
this.loadingStatus = false,
|
||||
required this.heroTag,
|
||||
required this.showAiBottomSheet,
|
||||
required this.showIntroDetail,
|
||||
required this.showEpisodes,
|
||||
required this.onShowMemberPage,
|
||||
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/refresh_indicator.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/utils/app_scheme.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class NoteListPage extends StatefulWidget {
|
||||
const NoteListPage({super.key, this.oid, this.upperMid});
|
||||
class NoteListPage extends CommonSlidePage {
|
||||
const NoteListPage({
|
||||
super.key,
|
||||
super.enableSlide,
|
||||
this.oid,
|
||||
this.upperMid,
|
||||
});
|
||||
|
||||
final dynamic oid;
|
||||
final dynamic upperMid;
|
||||
@@ -20,7 +26,7 @@ class NoteListPage extends StatefulWidget {
|
||||
State<NoteListPage> createState() => _NoteListPageState();
|
||||
}
|
||||
|
||||
class _NoteListPageState extends State<NoteListPage> {
|
||||
class _NoteListPageState extends CommonSlidePageState<NoteListPage> {
|
||||
late final _controller = Get.put(
|
||||
NoteListPageCtr(oid: widget.oid, upperMid: widget.upperMid),
|
||||
);
|
||||
@@ -32,37 +38,37 @@ class _NoteListPageState extends State<NoteListPage> {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: 16,
|
||||
toolbarHeight: 45,
|
||||
title: Obx(
|
||||
() => Text(
|
||||
'笔记${_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),
|
||||
Widget get buildPage => Scaffold(
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: 16,
|
||||
toolbarHeight: 45,
|
||||
title: Obx(
|
||||
() => Text(
|
||||
'笔记${_controller.count.value == -1 ? '' : '(${_controller.count.value})'}'),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
iconButton(
|
||||
context: context,
|
||||
tooltip: '关闭',
|
||||
icon: Icons.clear,
|
||||
onPressed: Get.back,
|
||||
size: 32,
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(1),
|
||||
child: Divider(
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
),
|
||||
body: Obx(() => _buildBody(_controller.loadingState.value)),
|
||||
);
|
||||
}
|
||||
actions: [
|
||||
iconButton(
|
||||
context: context,
|
||||
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) {
|
||||
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/http/loading_state.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_grpc.dart';
|
||||
import 'package:PiliPlus/pages/video/detail/reply_new/reply_page.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/global_data.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -19,7 +19,7 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
|
||||
class VideoReplyReplyPanel extends StatefulWidget {
|
||||
class VideoReplyReplyPanel extends CommonSlidePage {
|
||||
const VideoReplyReplyPanel({
|
||||
super.key,
|
||||
this.id,
|
||||
@@ -52,7 +52,8 @@ class VideoReplyReplyPanel extends StatefulWidget {
|
||||
State<VideoReplyReplyPanel> createState() => _VideoReplyReplyPanelState();
|
||||
}
|
||||
|
||||
class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel>
|
||||
class _VideoReplyReplyPanelState
|
||||
extends CommonSlidePageState<VideoReplyReplyPanel>
|
||||
with TickerProviderStateMixin {
|
||||
late VideoReplyReplyController _videoReplyReplyController;
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return GStorage.slideDismissReplyPage
|
||||
? Padding(
|
||||
padding: EdgeInsets.only(top: padding.value),
|
||||
child: _buildPage,
|
||||
)
|
||||
: _buildPage;
|
||||
}
|
||||
|
||||
Widget get _buildPage => Scaffold(
|
||||
Widget get buildPage => Scaffold(
|
||||
key: _key,
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: Column(
|
||||
@@ -165,73 +153,14 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel>
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
),
|
||||
Expanded(
|
||||
child: GStorage.slideDismissReplyPage
|
||||
? 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,
|
||||
child: enableSlide ? slideList() : buildList,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget get _buildList => ClipRect(
|
||||
@override
|
||||
Widget get buildList => ClipRect(
|
||||
child: refreshIndicator(
|
||||
onRefresh: () async {
|
||||
await _videoReplyReplyController.onRefresh();
|
||||
|
||||
@@ -3,9 +3,7 @@ import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
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/segment_progress_bar.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/models/bangumi/info.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/member/horizontal_member_page.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/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/global_data.dart';
|
||||
@@ -1105,7 +1104,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
}
|
||||
break;
|
||||
case 'note':
|
||||
videoDetailController.showNoteList();
|
||||
videoDetailController.showNoteList(context);
|
||||
break;
|
||||
}
|
||||
},
|
||||
@@ -1546,7 +1545,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
VideoIntroPanel(
|
||||
heroTag: heroTag,
|
||||
showAiBottomSheet: showAiBottomSheet,
|
||||
showIntroDetail: showIntroDetail,
|
||||
showEpisodes: showEpisodes,
|
||||
onShowMemberPage: onShowMemberPage,
|
||||
),
|
||||
@@ -1806,15 +1804,14 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
// ai总结
|
||||
showAiBottomSheet() {
|
||||
videoDetailController.childKey.currentState?.showBottomSheet(
|
||||
enableDrag: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
backgroundColor: Colors.transparent,
|
||||
(context) => AiDetail(modelResult: videoIntroController.modelResult),
|
||||
);
|
||||
}
|
||||
|
||||
showIntroDetail(videoDetail, videoTags) {
|
||||
videoDetailController.childKey.currentState?.showBottomSheet(
|
||||
enableDrag: true,
|
||||
shape: const RoundedRectangleBorder(),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
(context) => videoDetail is BangumiInfoModel
|
||||
? bangumi.IntroDetail(
|
||||
@@ -1829,7 +1826,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
}
|
||||
|
||||
showEpisodes(index, season, episodes, bvid, aid, cid) {
|
||||
Widget listSheetContent() => ListSheetContent(
|
||||
Widget listSheetContent([bool? enableSlide]) => ListSheetContent(
|
||||
enableSlide: enableSlide,
|
||||
index: index,
|
||||
season: season,
|
||||
bvid: bvid,
|
||||
@@ -1860,15 +1858,12 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
);
|
||||
if (isFullScreen) {
|
||||
Utils.showFSSheet(
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: listSheetContent(),
|
||||
),
|
||||
isFullScreen: isFullScreen,
|
||||
child: listSheetContent(false),
|
||||
isFullScreen: () => isFullScreen,
|
||||
);
|
||||
} else {
|
||||
videoDetailController.childKey.currentState?.showBottomSheet(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
backgroundColor: Colors.transparent,
|
||||
(context) => listSheetContent(),
|
||||
);
|
||||
}
|
||||
@@ -1945,150 +1940,22 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
}
|
||||
|
||||
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) {
|
||||
Utils.showFSSheet(child: listSheetContent(), isFullScreen: isFullScreen);
|
||||
Utils.showFSSheet(
|
||||
child: ViewPointsPage(
|
||||
enableSlide: false,
|
||||
videoDetailController: videoDetailController,
|
||||
plPlayerController: plPlayerController,
|
||||
),
|
||||
isFullScreen: () => isFullScreen,
|
||||
);
|
||||
} else {
|
||||
videoDetailController.childKey.currentState?.showBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
(context) => listSheetContent(),
|
||||
(context) => ViewPointsPage(
|
||||
videoDetailController: videoDetailController,
|
||||
plPlayerController: plPlayerController,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2109,6 +1976,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
|
||||
void onShowMemberPage(mid) {
|
||||
videoDetailController.childKey.currentState?.showBottomSheet(
|
||||
shape: const RoundedRectangleBorder(),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
(context) {
|
||||
return HorizontalMemberPage(
|
||||
@@ -2117,7 +1985,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
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 '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/segment_progress_bar.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/models/bangumi/info.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/member/horizontal_member_page.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/utils/extension.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:flutter/material.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
|
||||
import 'package:PiliPlus/http/user.dart';
|
||||
import 'package:PiliPlus/models/common/search_type.dart';
|
||||
import 'package:PiliPlus/pages/bangumi/introduction/index.dart';
|
||||
@@ -1398,7 +1396,7 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
}
|
||||
break;
|
||||
case 'note':
|
||||
videoDetailController.showNoteList();
|
||||
videoDetailController.showNoteList(context);
|
||||
break;
|
||||
}
|
||||
},
|
||||
@@ -1862,7 +1860,6 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
VideoIntroPanel(
|
||||
heroTag: heroTag,
|
||||
showAiBottomSheet: showAiBottomSheet,
|
||||
showIntroDetail: showIntroDetail,
|
||||
showEpisodes: showEpisodes,
|
||||
onShowMemberPage: onShowMemberPage,
|
||||
),
|
||||
@@ -2126,15 +2123,14 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
// ai总结
|
||||
showAiBottomSheet() {
|
||||
videoDetailController.childKey.currentState?.showBottomSheet(
|
||||
enableDrag: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
backgroundColor: Colors.transparent,
|
||||
(context) => AiDetail(modelResult: videoIntroController.modelResult),
|
||||
);
|
||||
}
|
||||
|
||||
showIntroDetail(videoDetail, videoTags) {
|
||||
videoDetailController.childKey.currentState?.showBottomSheet(
|
||||
enableDrag: true,
|
||||
shape: const RoundedRectangleBorder(),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
(context) => videoDetail is BangumiInfoModel
|
||||
? bangumi.IntroDetail(
|
||||
@@ -2149,7 +2145,8 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
}
|
||||
|
||||
showEpisodes(index, season, episodes, bvid, aid, cid) {
|
||||
Widget listSheetContent() => ListSheetContent(
|
||||
Widget listSheetContent([bool? enableSlide]) => ListSheetContent(
|
||||
enableSlide: enableSlide,
|
||||
index: index,
|
||||
season: season,
|
||||
bvid: bvid,
|
||||
@@ -2180,15 +2177,12 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
);
|
||||
if (isFullScreen) {
|
||||
Utils.showFSSheet(
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: listSheetContent(),
|
||||
),
|
||||
isFullScreen: isFullScreen,
|
||||
child: listSheetContent(false),
|
||||
isFullScreen: () => isFullScreen,
|
||||
);
|
||||
} else {
|
||||
videoDetailController.childKey.currentState?.showBottomSheet(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
backgroundColor: Colors.transparent,
|
||||
(context) => listSheetContent(),
|
||||
);
|
||||
}
|
||||
@@ -2265,155 +2259,22 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
}
|
||||
|
||||
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) {
|
||||
Utils.showFSSheet(child: listSheetContent(), isFullScreen: isFullScreen);
|
||||
Utils.showFSSheet(
|
||||
child: ViewPointsPage(
|
||||
enableSlide: false,
|
||||
videoDetailController: videoDetailController,
|
||||
plPlayerController: plPlayerController,
|
||||
),
|
||||
isFullScreen: () => isFullScreen,
|
||||
);
|
||||
} else {
|
||||
videoDetailController.childKey.currentState?.showBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
(context) => GStorage.collapsibleVideoPage
|
||||
? ViewPointsPage(child: listSheetContent())
|
||||
: listSheetContent(),
|
||||
(context) => ViewPointsPage(
|
||||
videoDetailController: videoDetailController,
|
||||
plPlayerController: plPlayerController,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2434,6 +2295,7 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
|
||||
void onShowMemberPage(mid) {
|
||||
videoDetailController.childKey.currentState?.showBottomSheet(
|
||||
shape: const RoundedRectangleBorder(),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
(context) {
|
||||
return HorizontalMemberPage(
|
||||
@@ -2442,41 +2304,6 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
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/pages/common/common_slide_page.dart';
|
||||
import 'package:PiliPlus/pages/video/detail/controller.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:PiliPlus/models/video/ai.dart';
|
||||
import 'package:PiliPlus/pages/video/detail/index.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
|
||||
class AiDetail extends StatelessWidget {
|
||||
class AiDetail extends CommonSlidePage {
|
||||
final ModelResult modelResult;
|
||||
|
||||
const AiDetail({
|
||||
@@ -16,157 +17,10 @@ class AiDetail extends StatelessWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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),
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
State<AiDetail> createState() => _AiDetailState();
|
||||
}
|
||||
|
||||
class _AiDetailState extends CommonSlidePageState<AiDetail> {
|
||||
InlineSpan buildContent(BuildContext context, content) {
|
||||
List descV2 = content.descV2;
|
||||
// type
|
||||
@@ -233,4 +87,158 @@ class AiDetail extends StatelessWidget {
|
||||
});
|
||||
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() {
|
||||
Utils.showFSSheet(
|
||||
isFullScreen: isFullScreen,
|
||||
isFullScreen: () => isFullScreen,
|
||||
child: Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
@@ -162,7 +162,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
dense: true,
|
||||
onTap: () {
|
||||
Get.back();
|
||||
widget.videoDetailCtr.showNoteList();
|
||||
widget.videoDetailCtr.showNoteList(context);
|
||||
},
|
||||
leading: const Icon(Icons.note_alt_outlined, size: 20),
|
||||
title: const Text('查看笔记', style: titleStyle),
|
||||
@@ -570,7 +570,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
void scheduleExit() async {
|
||||
const List<int> scheduleTimeChoices = [0, 15, 30, 45, 60];
|
||||
Utils.showFSSheet(
|
||||
isFullScreen: isFullScreen,
|
||||
isFullScreen: () => isFullScreen,
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return Container(
|
||||
@@ -778,7 +778,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
}
|
||||
|
||||
Utils.showFSSheet(
|
||||
isFullScreen: isFullScreen,
|
||||
isFullScreen: () => isFullScreen,
|
||||
child: Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
@@ -882,7 +882,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
final AudioQuality currentAudioQa = widget.videoDetailCtr.currentAudioQa!;
|
||||
final List<AudioItem> audio = videoInfo.dash!.audio!;
|
||||
Utils.showFSSheet(
|
||||
isFullScreen: isFullScreen,
|
||||
isFullScreen: () => isFullScreen,
|
||||
child: Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
@@ -973,7 +973,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
}
|
||||
|
||||
Utils.showFSSheet(
|
||||
isFullScreen: isFullScreen,
|
||||
isFullScreen: () => isFullScreen,
|
||||
child: Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
@@ -1082,7 +1082,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
final DanmakuController? danmakuController =
|
||||
widget.controller.danmakuController;
|
||||
Utils.showFSSheet(
|
||||
isFullScreen: isFullScreen,
|
||||
isFullScreen: () => isFullScreen,
|
||||
padding: isFullScreen ? 70 : null,
|
||||
child: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
@@ -1702,7 +1702,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
/// 播放顺序
|
||||
void showSetRepeat() async {
|
||||
Utils.showFSSheet(
|
||||
isFullScreen: isFullScreen,
|
||||
isFullScreen: () => isFullScreen,
|
||||
child: Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
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/stat/danmu.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_smart_dialog/flutter_smart_dialog.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:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
class MediaListPanel extends StatefulWidget {
|
||||
class MediaListPanel extends CommonSlidePage {
|
||||
const MediaListPanel({
|
||||
super.key,
|
||||
required this.mediaList,
|
||||
@@ -42,7 +43,7 @@ class MediaListPanel extends StatefulWidget {
|
||||
State<MediaListPanel> createState() => _MediaListPanelState();
|
||||
}
|
||||
|
||||
class _MediaListPanelState extends State<MediaListPanel> {
|
||||
class _MediaListPanelState extends CommonSlidePageState<MediaListPanel> {
|
||||
final _scrollController = ItemScrollController();
|
||||
late RxBool desc;
|
||||
|
||||
@@ -62,7 +63,7 @@ class _MediaListPanelState extends State<MediaListPanel> {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget get buildPage {
|
||||
return Column(
|
||||
children: [
|
||||
AppBar(
|
||||
@@ -92,19 +93,22 @@ class _MediaListPanelState extends State<MediaListPanel> {
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: widget.loadPrevious != null
|
||||
? refreshIndicator(
|
||||
onRefresh: () async {
|
||||
await widget.loadPrevious!();
|
||||
},
|
||||
child: _buildList,
|
||||
)
|
||||
: _buildList,
|
||||
child: enableSlide ? slideList() : buildList,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget get buildList => widget.loadPrevious != null
|
||||
? refreshIndicator(
|
||||
onRefresh: () async {
|
||||
await widget.loadPrevious!();
|
||||
},
|
||||
child: _buildList,
|
||||
)
|
||||
: _buildList;
|
||||
|
||||
Widget get _buildList => Obx(
|
||||
() => ScrollablePositionedList.builder(
|
||||
itemScrollController: _scrollController,
|
||||
|
||||
@@ -202,7 +202,7 @@ class Utils {
|
||||
|
||||
static void showFSSheet({
|
||||
required Widget child,
|
||||
required bool isFullScreen,
|
||||
required Function isFullScreen,
|
||||
double? padding,
|
||||
}) {
|
||||
Navigator.of(Get.context!).push(
|
||||
@@ -212,15 +212,28 @@ class Utils {
|
||||
? Column(
|
||||
children: [
|
||||
const Spacer(flex: 3),
|
||||
Expanded(flex: 7, child: child),
|
||||
if (isFullScreen && padding != null)
|
||||
Expanded(
|
||||
flex: 7,
|
||||
child: MediaQuery.removePadding(
|
||||
context: Get.context!,
|
||||
removeTop: true,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
if (isFullScreen() && padding != null)
|
||||
SizedBox(height: padding),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
Expanded(child: child),
|
||||
Expanded(
|
||||
child: MediaQuery.removePadding(
|
||||
context: Get.context!,
|
||||
removeLeft: true,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user