mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-05-13 20:53:58 +08:00
Compare commits
13 Commits
release7
...
release8-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d7583e010 | ||
|
|
64ff4e0d5c | ||
|
|
84ee106ddf | ||
|
|
cbdd8e77db | ||
|
|
a0b1e23727 | ||
|
|
aa05ae3f32 | ||
|
|
7d7ae3f130 | ||
|
|
f9ed31c65a | ||
|
|
43977c737b | ||
|
|
62a1768307 | ||
|
|
26e8553d9e | ||
|
|
018424d5bd | ||
|
|
a6f5bd8d7d |
@@ -47,6 +47,10 @@
|
||||
|
||||
## feat
|
||||
|
||||
- [x] 显示视频分段信息
|
||||
- [x] 调节字幕大小
|
||||
- [x] 调节全屏弹幕大小
|
||||
- [x] 收藏夹/稍后再看多选删除
|
||||
- [x] 搜索用户动态
|
||||
- [x] 直播弹幕
|
||||
- [x] 修改头像/用户名/签名/性别/生日
|
||||
|
||||
@@ -26,7 +26,7 @@ Widget articleContent({
|
||||
text: item.word?.words,
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.3,
|
||||
fontSize: FontSize.large.value,
|
||||
fontSize: 17,
|
||||
height: LineHeight.percent(125).size,
|
||||
fontStyle:
|
||||
item.word?.style?.italic == true ? FontStyle.italic : null,
|
||||
|
||||
@@ -73,7 +73,7 @@ Widget htmlRender({
|
||||
],
|
||||
style: {
|
||||
'html': Style(
|
||||
fontSize: FontSize.large,
|
||||
fontSize: FontSize(17),
|
||||
lineHeight: LineHeight.percent(160),
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
@@ -91,7 +91,7 @@ Widget htmlRender({
|
||||
// margin: Margins.zero,
|
||||
),
|
||||
'span': Style(
|
||||
fontSize: FontSize.medium,
|
||||
fontSize: FontSize.large,
|
||||
height: Height(1.8),
|
||||
),
|
||||
'div': Style(height: Height.auto()),
|
||||
@@ -109,12 +109,12 @@ Widget htmlRender({
|
||||
margin: Margins.only(bottom: 8),
|
||||
),
|
||||
'h3,h4,h5': Style(
|
||||
fontSize: FontSize.large,
|
||||
fontSize: FontSize(17),
|
||||
fontWeight: FontWeight.bold,
|
||||
margin: Margins.only(bottom: 4),
|
||||
),
|
||||
'figcaption': Style(
|
||||
fontSize: FontSize.medium,
|
||||
fontSize: FontSize.large,
|
||||
textAlign: TextAlign.center,
|
||||
// margin: Margins.only(top: 4),
|
||||
),
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:PiliPalaX/models/bangumi/info.dart' as bangumi;
|
||||
import 'package:PiliPalaX/models/video_detail_res.dart' as video;
|
||||
import 'package:flutter/material.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';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
@@ -23,7 +24,6 @@ class ListSheetContent extends StatefulWidget {
|
||||
this.aid,
|
||||
required this.currentCid,
|
||||
required this.changeFucCall,
|
||||
required this.onClose,
|
||||
});
|
||||
|
||||
final dynamic index;
|
||||
@@ -33,7 +33,6 @@ class ListSheetContent extends StatefulWidget {
|
||||
final int? aid;
|
||||
final int currentCid;
|
||||
final Function changeFucCall;
|
||||
final VoidCallback? onClose;
|
||||
|
||||
@override
|
||||
State<ListSheetContent> createState() => _ListSheetContentState();
|
||||
@@ -137,7 +136,7 @@ class _ListSheetContentState extends State<ListSheetContent>
|
||||
}
|
||||
}
|
||||
SmartDialog.showToast('切换到:$title');
|
||||
widget.onClose?.call();
|
||||
Get.back();
|
||||
widget.changeFucCall(
|
||||
episode is bangumi.EpisodeItem ? episode.epId : null,
|
||||
episode.runtimeType.toString() == "EpisodeItem"
|
||||
@@ -327,7 +326,7 @@ class _ListSheetContentState extends State<ListSheetContent>
|
||||
_mediumButton(
|
||||
tooltip: '关闭',
|
||||
icon: Icons.close,
|
||||
onPressed: widget.onClose,
|
||||
onPressed: Get.back,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -4,13 +4,26 @@ class Segment {
|
||||
final double start;
|
||||
final double end;
|
||||
final Color color;
|
||||
final String? title;
|
||||
final String? url;
|
||||
final int? from;
|
||||
final int? to;
|
||||
|
||||
Segment(this.start, this.end, this.color);
|
||||
Segment(
|
||||
this.start,
|
||||
this.end,
|
||||
this.color, [
|
||||
this.title,
|
||||
this.url,
|
||||
this.from,
|
||||
this.to,
|
||||
]);
|
||||
}
|
||||
|
||||
class SegmentProgressBar extends CustomPainter {
|
||||
final double progress;
|
||||
final List<Segment> segmentColors;
|
||||
double? _defHeight;
|
||||
|
||||
SegmentProgressBar({
|
||||
required this.progress,
|
||||
@@ -21,10 +34,10 @@ class SegmentProgressBar extends CustomPainter {
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()..style = PaintingStyle.fill;
|
||||
|
||||
for (var segment in segmentColors) {
|
||||
paint.color = segment.color;
|
||||
final segmentStart = segment.start * size.width;
|
||||
final segmentEnd = segment.end * size.width;
|
||||
for (int i = 0; i < segmentColors.length; i++) {
|
||||
paint.color = segmentColors[i].color;
|
||||
final segmentStart = segmentColors[i].start * size.width;
|
||||
final segmentEnd = segmentColors[i].end * size.width;
|
||||
final progressEnd = progress * size.width;
|
||||
|
||||
if (progressEnd < segmentStart) {
|
||||
@@ -33,17 +46,85 @@ class SegmentProgressBar extends CustomPainter {
|
||||
|
||||
final segmentWidth =
|
||||
(progressEnd < segmentEnd ? progressEnd : segmentEnd) - segmentStart;
|
||||
if (segmentWidth > 0) {
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(segmentStart, 0, segmentWidth, size.height),
|
||||
paint,
|
||||
);
|
||||
if (segmentWidth >= 0) {
|
||||
if (segmentColors[i].title != null) {
|
||||
double fontSize = 8;
|
||||
TextPainter textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: segmentColors[i].title,
|
||||
style: TextStyle(color: Colors.white, fontSize: fontSize),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
_defHeight ??= textPainter.height;
|
||||
|
||||
double? prevStart;
|
||||
if (i != 0) {
|
||||
prevStart = segmentColors[i - 1].start * size.width;
|
||||
}
|
||||
double width = i == 0 ? segmentStart : segmentStart - prevStart!;
|
||||
|
||||
while (textPainter.width > width - 2 && fontSize >= 2) {
|
||||
fontSize -= 1;
|
||||
textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: segmentColors[i].title,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: fontSize,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
textPainter.layout();
|
||||
}
|
||||
|
||||
if (i == 0) {
|
||||
canvas.drawRect(
|
||||
Rect.fromLTRB(
|
||||
0,
|
||||
-_defHeight!,
|
||||
size.width,
|
||||
0,
|
||||
),
|
||||
Paint()..color = Colors.grey[600]!.withOpacity(0.45),
|
||||
);
|
||||
}
|
||||
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(
|
||||
segmentStart,
|
||||
-_defHeight!,
|
||||
segmentWidth == 0 ? 2 : segmentWidth,
|
||||
size.height + _defHeight!,
|
||||
),
|
||||
paint,
|
||||
);
|
||||
|
||||
double textX = i == 0
|
||||
? (segmentStart - textPainter.width) / 2
|
||||
: (segmentStart - prevStart! - textPainter.width) / 2 +
|
||||
prevStart +
|
||||
1;
|
||||
double textY = -_defHeight! / 2 - textPainter.height / 2;
|
||||
textPainter.paint(canvas, Offset(textX, textY));
|
||||
} else {
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(
|
||||
segmentStart,
|
||||
0,
|
||||
segmentWidth == 0 ? 2 : segmentWidth,
|
||||
size.height,
|
||||
),
|
||||
paint,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,13 +240,9 @@ class VideoCustomActions {
|
||||
act: 5,
|
||||
reSrc: 11,
|
||||
);
|
||||
List<int> blackMidsList = GStorage.localCache
|
||||
.get(LocalCacheKey.blackMidsList, defaultValue: [-1])
|
||||
.map<int>((i) => i as int)
|
||||
.toList();
|
||||
List<int> blackMidsList = GStorage.blackMidsList;
|
||||
blackMidsList.insert(0, videoItem.owner.mid);
|
||||
GStorage.localCache
|
||||
.put(LocalCacheKey.blackMidsList, blackMidsList);
|
||||
GStorage.setBlackMidsList(blackMidsList);
|
||||
Get.back();
|
||||
SmartDialog.showToast(res['msg'] ?? '成功');
|
||||
},
|
||||
|
||||
@@ -104,8 +104,7 @@ class SearchHttp {
|
||||
try {
|
||||
switch (searchType) {
|
||||
case SearchType.video:
|
||||
List<int> blackMidsList = localCache
|
||||
.get(LocalCacheKey.blackMidsList, defaultValue: <int>[]);
|
||||
List<int> blackMidsList = GStorage.blackMidsList;
|
||||
if (res.data['data']['result'] != null) {
|
||||
for (var i in res.data['data']['result']) {
|
||||
// 屏蔽推广和拉黑用户
|
||||
|
||||
@@ -4,8 +4,7 @@ import 'package:PiliPalaX/grpc/app/card/v1/card.pb.dart' as card;
|
||||
import 'package:PiliPalaX/grpc/grpc_repo.dart';
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import '../common/constants.dart';
|
||||
import '../models/common/reply_type.dart';
|
||||
@@ -54,8 +53,7 @@ class VideoHttp {
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
List<RecVideoItemModel> list = [];
|
||||
List<int> blackMidsList =
|
||||
localCache.get(LocalCacheKey.blackMidsList, defaultValue: <int>[]);
|
||||
List<int> blackMidsList = GStorage.blackMidsList;
|
||||
for (var i in res.data['data']['item']) {
|
||||
//过滤掉live与ad,以及拉黑用户
|
||||
if (i['goto'] == 'av' &&
|
||||
@@ -141,8 +139,7 @@ class VideoHttp {
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
List<RecVideoItemAppModel> list = [];
|
||||
List<int> blackMidsList =
|
||||
localCache.get(LocalCacheKey.blackMidsList, defaultValue: <int>[]);
|
||||
List<int> blackMidsList = GStorage.blackMidsList;
|
||||
for (var i in res.data['data']['items']) {
|
||||
// 屏蔽推广和拉黑用户
|
||||
if (i['card_goto'] != 'ad_av' &&
|
||||
@@ -172,10 +169,7 @@ class VideoHttp {
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
List<HotVideoItemModel> list = [];
|
||||
List<int> blackMidsList = localCache
|
||||
.get(LocalCacheKey.blackMidsList, defaultValue: [-1])
|
||||
.map<int>((e) => e as int)
|
||||
.toList();
|
||||
List<int> blackMidsList = GStorage.blackMidsList;
|
||||
for (var i in res.data['data']['list']) {
|
||||
if (!blackMidsList.contains(i['owner']['mid'])) {
|
||||
list.add(HotVideoItemModel.fromJson(i));
|
||||
@@ -191,8 +185,7 @@ class VideoHttp {
|
||||
dynamic res = await GrpcRepo.popular(idx);
|
||||
if (res['status']) {
|
||||
List<card.Card> list = [];
|
||||
List<int> blackMidsList =
|
||||
localCache.get(LocalCacheKey.blackMidsList, defaultValue: <int>[]);
|
||||
List<int> blackMidsList = GStorage.blackMidsList;
|
||||
for (card.Card item in res['data']) {
|
||||
if (!blackMidsList.contains(item.smallCoverV5.up.id.toInt())) {
|
||||
list.add(item);
|
||||
@@ -861,11 +854,14 @@ class VideoHttp {
|
||||
static Future subtitlesJson(
|
||||
{String? aid, String? bvid, required int cid}) async {
|
||||
assert(aid != null || bvid != null);
|
||||
var res = await Request().get(Api.subtitleUrl, data: {
|
||||
if (aid != null) 'aid': aid,
|
||||
if (bvid != null) 'bvid': bvid,
|
||||
'cid': cid,
|
||||
});
|
||||
var res = await Request().get(
|
||||
Api.subtitleUrl,
|
||||
data: {
|
||||
if (aid != null) 'aid': aid,
|
||||
if (bvid != null) 'bvid': bvid,
|
||||
'cid': cid,
|
||||
},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
dynamic data = res.data['data'];
|
||||
List subtitlesJson = data['subtitle']['subtitles'];
|
||||
@@ -887,6 +883,7 @@ class VideoHttp {
|
||||
return {
|
||||
'status': true,
|
||||
'data': subtitlesJson,
|
||||
'view_points': data['view_points'],
|
||||
};
|
||||
} else {
|
||||
return {'status': false, 'data': [], 'msg': res.data['message']};
|
||||
@@ -910,6 +907,12 @@ class VideoHttp {
|
||||
return "${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}.${ms.toString().padLeft(3, '0')}";
|
||||
}
|
||||
|
||||
String processList(List list) {
|
||||
return list.fold('WEBVTT\n\n', (previous, item) {
|
||||
return '$previous${item?['sid'] ?? 0}\n${subtitleTimecode(item['from'])} --> ${subtitleTimecode(item['to'])}\n${item['content'].trim()}\n\n';
|
||||
});
|
||||
}
|
||||
|
||||
for (var i in subtitlesJson) {
|
||||
var res =
|
||||
await Request().get("https://${i['subtitle_url'].split('//')[1]}");
|
||||
@@ -944,21 +947,16 @@ class VideoHttp {
|
||||
]
|
||||
}
|
||||
*/
|
||||
if (res.data != null) {
|
||||
String vttData = "WEBVTT\n\n";
|
||||
for (var item in res.data['body']) {
|
||||
vttData += "${item['sid'] ?? 0}\n";
|
||||
vttData +=
|
||||
"${subtitleTimecode(item['from'])} --> ${subtitleTimecode(item['to'])}\n";
|
||||
vttData += "${item['content'].trim()}\n\n";
|
||||
}
|
||||
if (res.data != null && res.data?['body'] is List) {
|
||||
String vttData = await compute(processList, res.data['body'] as List);
|
||||
subtitlesVtt.add({
|
||||
'language': i['lan'],
|
||||
'title': i['lan_doc'],
|
||||
'text': vttData,
|
||||
});
|
||||
} else {
|
||||
SmartDialog.showToast("字幕${i['lan_doc']}加载失败, ${res.data['message']}");
|
||||
// SmartDialog.showToast("字幕${i['lan_doc']}加载失败, ${res.data['message']}");
|
||||
debugPrint('字幕${i['lan_doc']}加载失败, ${res.data['message']}');
|
||||
}
|
||||
}
|
||||
if (subtitlesVtt.isNotEmpty) {
|
||||
@@ -973,8 +971,7 @@ class VideoHttp {
|
||||
var res = await Request().get(rankApi);
|
||||
if (res.data['code'] == 0) {
|
||||
List<HotVideoItemModel> list = [];
|
||||
List<int> blackMidsList =
|
||||
localCache.get(LocalCacheKey.blackMidsList, defaultValue: <int>[]);
|
||||
List<int> blackMidsList = GStorage.blackMidsList;
|
||||
for (var i in res.data['data']['list']) {
|
||||
if (!blackMidsList.contains(i['owner']['mid'])) {
|
||||
list.add(HotVideoItemModel.fromJson(i));
|
||||
|
||||
@@ -207,7 +207,7 @@ class MyApp extends StatelessWidget {
|
||||
titleSpacing: 0,
|
||||
centerTitle: false,
|
||||
scrolledUnderElevation: 0,
|
||||
backgroundColor: Platform.isIOS ? colorScheme.surface : null,
|
||||
backgroundColor: isDynamic ? null : colorScheme.surface,
|
||||
titleTextStyle: TextStyle(fontSize: 16, color: colorScheme.onSurface),
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
|
||||
@@ -300,7 +300,7 @@ class BangumiIntroController extends CommonController {
|
||||
delMediaIdsNew = [];
|
||||
SmartDialog.showToast('操作成功');
|
||||
Get.back();
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
Future.delayed(const Duration(milliseconds: 255), () {
|
||||
queryBangumiLikeCoinFav();
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -25,8 +25,8 @@ class _BlackListPageState extends State<BlackListPage> {
|
||||
List list = _blackListController.loadingState.value is Success
|
||||
? (_blackListController.loadingState.value as Success).response
|
||||
: <int>[];
|
||||
GStorage.localCache.put(LocalCacheKey.blackMidsList,
|
||||
list.isNotEmpty ? list.map<int>((e) => e.mid!).toList() : list);
|
||||
GStorage.setBlackMidsList(
|
||||
list.isNotEmpty ? list.map<int>((e) => e.mid!).toList() : <int>[]);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -121,7 +121,8 @@ class BlackListController extends CommonController {
|
||||
if (response.response.list.length >= total.value) {
|
||||
isEnd = true;
|
||||
}
|
||||
loadingState.value = LoadingState.success(response.response.list);
|
||||
loadingState.value = LoadingState.success(
|
||||
response.response.list.isNotEmpty ? response.response.list : <int>[]);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -131,7 +132,8 @@ class BlackListController extends CommonController {
|
||||
List list = (loadingState.value as Success).response;
|
||||
list.removeWhere((e) => e.mid == mid);
|
||||
total.value = total.value - 1;
|
||||
loadingState.value = LoadingState.success(list);
|
||||
loadingState.value =
|
||||
LoadingState.success(list.isNotEmpty ? list : <int>[]);
|
||||
SmartDialog.showToast(result['msg']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,13 @@ abstract class MultiSelectController extends CommonController {
|
||||
}
|
||||
|
||||
void handleSelect([bool checked = false]) {
|
||||
List? list = (loadingState.value as Success?)?.response;
|
||||
if (list.isNullOrEmpty.not) {
|
||||
loadingState.value = LoadingState.success(
|
||||
list!.map((item) => item..checked = checked).toList());
|
||||
checkedCount.value = checked ? list.length : 0;
|
||||
if (loadingState.value is Success) {
|
||||
List list = (loadingState.value as Success).response;
|
||||
if (list.isNotEmpty) {
|
||||
loadingState.value = LoadingState.success(
|
||||
list.map((item) => item..checked = checked).toList());
|
||||
checkedCount.value = checked ? list.length : 0;
|
||||
}
|
||||
}
|
||||
if (checked.not) {
|
||||
enableMultiSelect.value = false;
|
||||
|
||||
@@ -200,8 +200,9 @@ abstract class ReplyController extends CommonController {
|
||||
savedReplies[key] = null;
|
||||
if (GlobalData().grpcReply) {
|
||||
ReplyInfo replyInfo = Utils.replyCast(res);
|
||||
MainListReply response =
|
||||
(loadingState.value as Success?)?.response ?? MainListReply();
|
||||
MainListReply response = loadingState.value is Success
|
||||
? (loadingState.value as Success).response
|
||||
: MainListReply();
|
||||
if (oid != null) {
|
||||
response.replies.insert(hasUpTop ? 1 : 0, replyInfo);
|
||||
} else {
|
||||
@@ -210,8 +211,9 @@ abstract class ReplyController extends CommonController {
|
||||
count.value += 1;
|
||||
loadingState.value = LoadingState.success(response);
|
||||
} else {
|
||||
ReplyData response =
|
||||
(loadingState.value as Success?)?.response ?? ReplyData();
|
||||
ReplyData response = loadingState.value is Success
|
||||
? (loadingState.value as Success).response
|
||||
: ReplyData();
|
||||
response.replies ??= <ReplyItemModel>[];
|
||||
if (oid != null) {
|
||||
response.replies
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:PiliPalaX/pages/video/detail/reply/widgets/reply_item.dart';
|
||||
import 'package:PiliPalaX/pages/video/detail/reply/widgets/reply_item_grpc.dart';
|
||||
import 'package:PiliPalaX/utils/extension.dart';
|
||||
import 'package:PiliPalaX/utils/global_data.dart';
|
||||
import 'package:PiliPalaX/utils/storage.dart';
|
||||
import 'package:PiliPalaX/utils/utils.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -47,6 +48,8 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
|
||||
int? opusId;
|
||||
bool isOpusId = false;
|
||||
|
||||
late final List<double> _ratio = GStorage.dynamicDetailRatio;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -202,7 +205,53 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
|
||||
);
|
||||
},
|
||||
),
|
||||
// actions: _detailModel != null ? appBarAction() : [],
|
||||
actions: context.orientation == Orientation.landscape
|
||||
? [
|
||||
IconButton(
|
||||
tooltip: '页面比例调节',
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(
|
||||
top: 56,
|
||||
right: 16,
|
||||
),
|
||||
width: context.width / 4,
|
||||
height: 32,
|
||||
child: Builder(
|
||||
builder: (context) => Slider(
|
||||
min: 1,
|
||||
max: 100,
|
||||
value: _ratio.first,
|
||||
onChanged: (value) async {
|
||||
if (value >= 10 && value <= 90) {
|
||||
_ratio[0] = value;
|
||||
_ratio[1] = 100 - value;
|
||||
await GStorage.setting.put(
|
||||
SettingBoxKey.dynamicDetailRatio,
|
||||
_ratio,
|
||||
);
|
||||
(context as Element).markNeedsBuild();
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: Transform.rotate(
|
||||
angle: pi / 2,
|
||||
child: Icon(Icons.splitscreen),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
body: refreshIndicator(
|
||||
onRefresh: () async {
|
||||
@@ -239,41 +288,42 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: _ratio[0].toInt(),
|
||||
child: CustomScrollView(
|
||||
controller: ScrollController(),
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(left: padding / 2),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: DynamicPanel(
|
||||
item: _dynamicDetailController.item,
|
||||
source: 'detail',
|
||||
),
|
||||
)),
|
||||
]),
|
||||
),
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
controller:
|
||||
_dynamicDetailController.scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(right: padding / 2),
|
||||
sliver: replyPersistentHeader(context)),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(right: padding / 2),
|
||||
sliver: Obx(
|
||||
() => replyList(_dynamicDetailController
|
||||
.loadingState.value),
|
||||
controller: ScrollController(),
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(left: padding / 4),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: DynamicPanel(
|
||||
item: _dynamicDetailController.item,
|
||||
source: 'detail',
|
||||
),
|
||||
),
|
||||
]
|
||||
// .map<Widget>(
|
||||
// (e) => SliverPadding(padding: padding, sliver: e))
|
||||
// .toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: _ratio[1].toInt(),
|
||||
child: CustomScrollView(
|
||||
controller: _dynamicDetailController.scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(right: padding / 4),
|
||||
sliver: replyPersistentHeader(context),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(right: padding / 4),
|
||||
sliver: Obx(
|
||||
() => replyList(_dynamicDetailController
|
||||
.loadingState.value),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -61,9 +61,9 @@ class Content extends StatelessWidget {
|
||||
selectionControls: MaterialTextSelectionControls(),
|
||||
child: Text.rich(
|
||||
/// fix 默认20px高度
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
height: 0,
|
||||
fontSize: 15,
|
||||
fontSize: source == 'detail' ? 16 : 15,
|
||||
),
|
||||
richNode(item, context),
|
||||
maxLines: source == 'detail' ? 999 : 6,
|
||||
|
||||
@@ -59,6 +59,8 @@ class FavDetailController extends MultiSelectController {
|
||||
if (result['status']) {
|
||||
List dataList = (loadingState.value as Success).response;
|
||||
dataList.removeWhere((item) => item.id == id);
|
||||
item.value.mediaCount = item.value.mediaCount! - 1;
|
||||
item.refresh();
|
||||
loadingState.value = LoadingState.success(dataList);
|
||||
SmartDialog.showToast('取消收藏');
|
||||
} else {
|
||||
@@ -103,6 +105,8 @@ class FavDetailController extends MultiSelectController {
|
||||
if (result['status']) {
|
||||
List dataList = (loadingState.value as Success).response;
|
||||
Set remainList = dataList.toSet().difference(list.toSet());
|
||||
item.value.mediaCount = item.value.mediaCount! - list.length;
|
||||
item.refresh();
|
||||
loadingState.value =
|
||||
LoadingState.success(remainList.toList());
|
||||
SmartDialog.showToast('取消收藏');
|
||||
|
||||
@@ -179,9 +179,11 @@ class HistoryController extends MultiSelectController {
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
_onDelete(((loadingState.value as Success).response as List)
|
||||
.where((e) => e.checked)
|
||||
.toList());
|
||||
if (loadingState.value is Success) {
|
||||
_onDelete(((loadingState.value as Success).response as List)
|
||||
.where((e) => e.checked)
|
||||
.toList());
|
||||
}
|
||||
},
|
||||
child: const Text('确认'),
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:PiliPalaX/pages/common/multi_select_controller.dart';
|
||||
import 'package:PiliPalaX/pages/fav_search/controller.dart';
|
||||
import 'package:PiliPalaX/utils/app_scheme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -33,7 +34,7 @@ class HistoryItem extends StatelessWidget {
|
||||
String heroTag = Utils.makeHeroTag(aid);
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
if (ctr!.enableMultiSelect.value) {
|
||||
if (ctr is MultiSelectController && ctr!.enableMultiSelect.value) {
|
||||
feedBack();
|
||||
onChoose?.call();
|
||||
return;
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:PiliPalaX/pages/video/detail/reply/widgets/reply_item.dart';
|
||||
import 'package:PiliPalaX/pages/video/detail/reply/widgets/reply_item_grpc.dart';
|
||||
import 'package:PiliPalaX/utils/extension.dart';
|
||||
import 'package:PiliPalaX/utils/global_data.dart';
|
||||
import 'package:PiliPalaX/utils/storage.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
@@ -42,6 +43,8 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
|
||||
bool _isFabVisible = true;
|
||||
late AnimationController fabAnimationCtr;
|
||||
|
||||
late final List<double> _ratio = GStorage.dynamicDetailRatio;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -134,6 +137,49 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
|
||||
title: Text(title),
|
||||
actions: [
|
||||
const SizedBox(width: 4),
|
||||
if (context.orientation == Orientation.landscape)
|
||||
IconButton(
|
||||
tooltip: '页面比例调节',
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(
|
||||
top: 56,
|
||||
right: 16,
|
||||
),
|
||||
width: context.width / 4,
|
||||
height: 32,
|
||||
child: Builder(
|
||||
builder: (context) => Slider(
|
||||
min: 1,
|
||||
max: 100,
|
||||
value: _ratio.first,
|
||||
onChanged: (value) async {
|
||||
if (value >= 10 && value <= 90) {
|
||||
_ratio[0] = value;
|
||||
_ratio[1] = 100 - value;
|
||||
await GStorage.setting.put(
|
||||
SettingBoxKey.dynamicDetailRatio,
|
||||
_ratio,
|
||||
);
|
||||
(context as Element).markNeedsBuild();
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: Transform.rotate(
|
||||
angle: pi / 2,
|
||||
child: Icon(Icons.splitscreen),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: '用内置浏览器打开',
|
||||
onPressed: () {
|
||||
@@ -217,6 +263,7 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: _ratio[0].toInt(),
|
||||
child: CustomScrollView(
|
||||
controller: orientation == Orientation.portrait
|
||||
? _htmlRenderCtr.scrollController
|
||||
@@ -225,7 +272,7 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
|
||||
SliverPadding(
|
||||
padding: orientation == Orientation.portrait
|
||||
? EdgeInsets.symmetric(horizontal: padding)
|
||||
: EdgeInsets.only(left: padding / 2),
|
||||
: EdgeInsets.only(left: padding / 4),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Obx(
|
||||
() => _htmlRenderCtr.loaded.value
|
||||
@@ -237,21 +284,31 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
|
||||
SliverPadding(
|
||||
padding: orientation == Orientation.portrait
|
||||
? EdgeInsets.symmetric(horizontal: padding)
|
||||
: EdgeInsets.only(left: padding / 2),
|
||||
: EdgeInsets.only(left: padding / 4),
|
||||
sliver: _buildContent,
|
||||
),
|
||||
if (orientation == Orientation.portrait) ...[
|
||||
SliverToBoxAdapter(
|
||||
child: Divider(
|
||||
thickness: 8,
|
||||
color: Theme.of(context)
|
||||
.dividerColor
|
||||
.withOpacity(0.05),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.symmetric(horizontal: padding),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Divider(
|
||||
thickness: 8,
|
||||
color: Theme.of(context)
|
||||
.dividerColor
|
||||
.withOpacity(0.05),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(child: replyHeader()),
|
||||
Obx(
|
||||
() => replyList(_htmlRenderCtr.loadingState.value),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.symmetric(horizontal: padding),
|
||||
sliver: SliverToBoxAdapter(child: replyHeader()),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.symmetric(horizontal: padding),
|
||||
sliver: Obx(
|
||||
() =>
|
||||
replyList(_htmlRenderCtr.loadingState.value),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -263,17 +320,18 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
|
||||
color:
|
||||
Theme.of(context).dividerColor.withOpacity(0.05)),
|
||||
Expanded(
|
||||
flex: _ratio[1].toInt(),
|
||||
child: CustomScrollView(
|
||||
controller: _htmlRenderCtr.scrollController,
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(right: padding / 2),
|
||||
padding: EdgeInsets.only(right: padding / 4),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: replyHeader(),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(right: padding / 2),
|
||||
padding: EdgeInsets.only(right: padding / 4),
|
||||
sliver: Obx(
|
||||
() =>
|
||||
replyList(_htmlRenderCtr.loadingState.value),
|
||||
|
||||
@@ -152,6 +152,7 @@ class LaterController extends MultiSelectController {
|
||||
Set remainList = ((loadingState.value as Success).response as List)
|
||||
.toSet()
|
||||
.difference(result.toSet());
|
||||
count.value -= aids.length;
|
||||
loadingState.value = LoadingState.success(remainList.toList());
|
||||
if (enableMultiSelect.value) {
|
||||
checkedCount.value = 0;
|
||||
|
||||
@@ -29,7 +29,8 @@ class MemberVideoCtr extends CommonController {
|
||||
aid = null;
|
||||
next = null;
|
||||
currentPage = 0;
|
||||
return super.onRefresh();
|
||||
isEnd = false;
|
||||
await queryData();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -43,8 +43,9 @@ class RcmdController extends PopupController {
|
||||
}
|
||||
|
||||
@override
|
||||
Future onRefresh() {
|
||||
Future onRefresh() async {
|
||||
currentPage = 0;
|
||||
return super.onRefresh();
|
||||
isEnd = false;
|
||||
await queryData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:PiliPalaX/pages/main/controller.dart';
|
||||
import 'package:PiliPalaX/pages/member/new/controller.dart'
|
||||
show MemberTabType, MemberTabTypeExt;
|
||||
@@ -212,6 +214,15 @@ class _ExtraSettingState extends State<ExtraSetting> {
|
||||
GlobalData().grpcReply = value;
|
||||
},
|
||||
),
|
||||
SetSwitchItem(
|
||||
title: '显示视频分段信息',
|
||||
leading: Transform.rotate(
|
||||
angle: pi / 2,
|
||||
child: Icon(Icons.reorder),
|
||||
),
|
||||
setKey: SettingBoxKey.showViewPoints,
|
||||
defaultVal: true,
|
||||
),
|
||||
Obx(
|
||||
() => ListTile(
|
||||
enableFeedback: true,
|
||||
|
||||
@@ -44,9 +44,7 @@ class _SetSwitchItemState extends State<SetSwitchItem> {
|
||||
// if (widget.setKey == SettingBoxKey.autoUpdate && value == true) {
|
||||
// Utils.checkUpdate();
|
||||
// }
|
||||
if (widget.onChanged != null) {
|
||||
widget.onChanged!.call(val);
|
||||
}
|
||||
widget.onChanged?.call(val);
|
||||
if (widget.needReboot != null && widget.needReboot!) {
|
||||
SmartDialog.showToast('重启生效');
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:PiliPalaX/common/widgets/pair.dart';
|
||||
import 'package:PiliPalaX/common/widgets/segment_progress_bar.dart';
|
||||
import 'package:PiliPalaX/http/danmaku.dart';
|
||||
import 'package:PiliPalaX/http/init.dart';
|
||||
import 'package:PiliPalaX/models/video/play/subtitle.dart';
|
||||
import 'package:PiliPalaX/utils/extension.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:floating/floating.dart';
|
||||
@@ -284,6 +285,7 @@ class VideoDetailController extends GetxController
|
||||
List<Pair<SegmentType, SkipType>>? _blockSettings;
|
||||
List<Color>? _blockColor;
|
||||
RxList<SegmentModel> segmentList = <SegmentModel>[].obs;
|
||||
List<Segment> viewPointList = <Segment>[];
|
||||
List<Segment>? _segmentProgressList;
|
||||
Color _getColor(SegmentType segment) =>
|
||||
_blockColor?[segment.index] ?? segment.color;
|
||||
@@ -607,11 +609,7 @@ class VideoDetailController extends GetxController
|
||||
.clamp(0.0, 1.0);
|
||||
double end = (item.segment.second / ((data.timeLength ?? 0) / 1000))
|
||||
.clamp(0.0, 1.0);
|
||||
return Segment(
|
||||
start,
|
||||
(start == end && end != 0) ? (end + 0.01).clamp(0.0, 1.0) : end,
|
||||
_getColor(item.segmentType),
|
||||
);
|
||||
return Segment(start, end, _getColor(item.segmentType));
|
||||
}).toList());
|
||||
} catch (e) {
|
||||
debugPrint('failed to parse sponsorblock: $e');
|
||||
@@ -848,6 +846,9 @@ class VideoDetailController extends GetxController
|
||||
},
|
||||
),
|
||||
segmentList: _segmentProgressList,
|
||||
viewPointList: viewPointList,
|
||||
vttSubtitles: _vttSubtitles,
|
||||
vttSubtitlesIndex: vttSubtitlesIndex,
|
||||
// 硬解
|
||||
enableHA: enableHA.value,
|
||||
hwdec: hwdec.value,
|
||||
@@ -881,6 +882,7 @@ class VideoDetailController extends GetxController
|
||||
if (enableSponsorBlock) {
|
||||
await _querySponsorBlock();
|
||||
}
|
||||
_getSubtitle();
|
||||
if (data.acceptDesc!.isNotEmpty && data.acceptDesc!.contains('试看')) {
|
||||
SmartDialog.showToast(
|
||||
'该视频为专属视频,仅提供试看',
|
||||
@@ -1024,7 +1026,6 @@ class VideoDetailController extends GetxController
|
||||
List<PostSegmentModel>? list;
|
||||
|
||||
void onBlock(BuildContext context) {
|
||||
PersistentBottomSheetController? ctr;
|
||||
list ??= <PostSegmentModel>[];
|
||||
if (list!.isEmpty) {
|
||||
list!.add(
|
||||
@@ -1038,18 +1039,18 @@ class VideoDetailController extends GetxController
|
||||
),
|
||||
);
|
||||
}
|
||||
ctr = plPlayerController.isFullScreen.value
|
||||
plPlayerController.isFullScreen.value
|
||||
? scaffoldKey.currentState?.showBottomSheet(
|
||||
enableDrag: false,
|
||||
(context) => _postPanel(ctr?.close, false),
|
||||
(context) => _postPanel(false),
|
||||
)
|
||||
: childKey.currentState?.showBottomSheet(
|
||||
enableDrag: false,
|
||||
(context) => _postPanel(ctr?.close),
|
||||
(context) => _postPanel(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _postPanel(onClose, [bool isChild = true]) => StatefulBuilder(
|
||||
Widget _postPanel([bool isChild = true]) => StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
void updateSegment({
|
||||
required bool isFirst,
|
||||
@@ -1201,7 +1202,7 @@ class VideoDetailController extends GetxController
|
||||
iconButton(
|
||||
context: context,
|
||||
tooltip: '关闭',
|
||||
onPressed: onClose,
|
||||
onPressed: Get.back,
|
||||
icon: Icons.close,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
@@ -1576,4 +1577,76 @@ class VideoDetailController extends GetxController
|
||||
SegmentType.exclusive_access => [ActionType.full],
|
||||
};
|
||||
}
|
||||
|
||||
List<Map<String, String>> _vttSubtitles = <Map<String, String>>[];
|
||||
int vttSubtitlesIndex = 0;
|
||||
|
||||
void _getSubtitle() {
|
||||
_vttSubtitles.clear();
|
||||
vttSubtitlesIndex = 0;
|
||||
viewPointList.clear();
|
||||
_querySubtitles().then((value) {
|
||||
if (_vttSubtitles.isNotEmpty) {
|
||||
String preference = setting.get(
|
||||
SettingBoxKey.subtitlePreference,
|
||||
defaultValue: SubtitlePreference.values.first.code,
|
||||
);
|
||||
if (preference == 'on') {
|
||||
vttSubtitlesIndex = 1;
|
||||
} else if (preference == 'withoutAi') {
|
||||
for (int i = 1; i < _vttSubtitles.length; i++) {
|
||||
if (_vttSubtitles[i]['language']!.startsWith('ai')) {
|
||||
continue;
|
||||
}
|
||||
vttSubtitlesIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (plPlayerController.vttSubtitles.isEmpty) {
|
||||
plPlayerController.vttSubtitles.value = _vttSubtitles;
|
||||
plPlayerController.vttSubtitlesIndex.value = vttSubtitlesIndex;
|
||||
if (vttSubtitlesIndex != 0) {
|
||||
plPlayerController.setSubtitle(vttSubtitlesIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future _querySubtitles() async {
|
||||
Map res = await VideoHttp.subtitlesJson(bvid: bvid, cid: cid.value);
|
||||
// if (!res["status"]) {
|
||||
// SmartDialog.showToast('查询字幕错误,${res["msg"]}');
|
||||
// }
|
||||
|
||||
if (res["data"] is List && res["data"].isNotEmpty) {
|
||||
var result = await VideoHttp.vttSubtitles(res["data"]);
|
||||
if (result != null) {
|
||||
_vttSubtitles = result;
|
||||
}
|
||||
// if (_vttSubtitles.isEmpty) {
|
||||
// SmartDialog.showToast('字幕均加载失败');
|
||||
// }
|
||||
}
|
||||
if (GStorage.showViewPoints &&
|
||||
res["view_points"] is List &&
|
||||
res["view_points"].isNotEmpty) {
|
||||
viewPointList = (res["view_points"] as List).map((item) {
|
||||
double start =
|
||||
(item['to'] / ((data.timeLength ?? 0) / 1000)).clamp(0.0, 1.0);
|
||||
return Segment(
|
||||
start,
|
||||
start,
|
||||
Colors.black87,
|
||||
item?['content'],
|
||||
item?['imgUrl'],
|
||||
item?['from'],
|
||||
item?['to'],
|
||||
);
|
||||
}).toList();
|
||||
if (plPlayerController.viewPointList.isEmpty) {
|
||||
plPlayerController.viewPointList.value = viewPointList;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -42,24 +43,31 @@ class ActionItemState extends State<ActionItem> with TickerProviderStateMixin {
|
||||
bool get _isThumbUp => widget.semanticsLabel == '点赞';
|
||||
late int _lastTime;
|
||||
bool _hideCircle = false;
|
||||
Timer? _timer;
|
||||
|
||||
void _startLongPress() {
|
||||
_lastTime = DateTime.now().millisecondsSinceEpoch;
|
||||
if (!widget.hasOneThree) {
|
||||
controller?.forward();
|
||||
widget.callBack?.call(true);
|
||||
_timer ??= Timer(const Duration(milliseconds: 100), () {
|
||||
feedBack();
|
||||
controller?.forward();
|
||||
widget.callBack?.call(true);
|
||||
cancelTimer();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _cancelLongPress([bool isCancel = false]) {
|
||||
int duration = DateTime.now().millisecondsSinceEpoch - _lastTime;
|
||||
if (duration < 1500) {
|
||||
if (duration >= 100 && duration < 1500) {
|
||||
controller?.reverse();
|
||||
widget.callBack?.call(false);
|
||||
}
|
||||
if (duration <= 50 && !isCancel) {
|
||||
feedBack();
|
||||
widget.onTap?.call();
|
||||
} else if (duration < 100) {
|
||||
cancelTimer();
|
||||
if (!isCancel) {
|
||||
feedBack();
|
||||
widget.onTap?.call();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +78,7 @@ class ActionItemState extends State<ActionItem> with TickerProviderStateMixin {
|
||||
controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
reverseDuration: const Duration(milliseconds: 500),
|
||||
reverseDuration: const Duration(milliseconds: 400),
|
||||
);
|
||||
|
||||
_animation = Tween<double>(begin: 0, end: -2 * pi).animate(controller!)
|
||||
@@ -88,8 +96,14 @@ class ActionItemState extends State<ActionItem> with TickerProviderStateMixin {
|
||||
}
|
||||
}
|
||||
|
||||
void cancelTimer() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
cancelTimer();
|
||||
_animation?.removeListener(() {});
|
||||
controller?.dispose();
|
||||
super.dispose();
|
||||
|
||||
@@ -298,19 +298,19 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
|
||||
_savedReplies[key] = null;
|
||||
if (GlobalData().grpcReply) {
|
||||
ReplyInfo replyInfo = Utils.replyCast(res);
|
||||
List list =
|
||||
(_videoReplyReplyController.loadingState.value as Success?)
|
||||
?.response ??
|
||||
<ReplyInfo>[];
|
||||
List list = _videoReplyReplyController.loadingState.value is Success
|
||||
? (_videoReplyReplyController.loadingState.value as Success)
|
||||
.response
|
||||
: <ReplyInfo>[];
|
||||
list.insert(index + 1, replyInfo);
|
||||
_videoReplyReplyController.count.value += 1;
|
||||
_videoReplyReplyController.loadingState.value =
|
||||
LoadingState.success(list);
|
||||
} else {
|
||||
List list =
|
||||
(_videoReplyReplyController.loadingState.value as Success?)
|
||||
?.response ??
|
||||
<ReplyItemModel>[];
|
||||
List list = _videoReplyReplyController.loadingState.value is Success
|
||||
? (_videoReplyReplyController.loadingState.value as Success)
|
||||
.response
|
||||
: <ReplyItemModel>[];
|
||||
list.insert(index + 1, ReplyItemModel.fromJson(res, ''));
|
||||
_videoReplyReplyController.count.value += 1;
|
||||
_videoReplyReplyController.loadingState.value =
|
||||
|
||||
@@ -3,7 +3,9 @@ import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:PiliPalaX/common/constants.dart';
|
||||
import 'package:PiliPalaX/common/widgets/icon_button.dart';
|
||||
import 'package:PiliPalaX/common/widgets/list_sheet.dart';
|
||||
import 'package:PiliPalaX/common/widgets/segment_progress_bar.dart';
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:PiliPalaX/models/bangumi/info.dart';
|
||||
import 'package:PiliPalaX/models/common/reply_type.dart';
|
||||
@@ -16,6 +18,7 @@ import 'package:PiliPalaX/pages/video/detail/widgets/ai_detail.dart';
|
||||
import 'package:PiliPalaX/utils/extension.dart';
|
||||
import 'package:PiliPalaX/utils/global_data.dart';
|
||||
import 'package:PiliPalaX/utils/id_utils.dart';
|
||||
import 'package:PiliPalaX/utils/utils.dart';
|
||||
import 'package:auto_orientation/auto_orientation.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:floating/floating.dart';
|
||||
@@ -350,6 +353,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
}
|
||||
if (plPlayerController != null) {
|
||||
_makeHeartBeat();
|
||||
videoDetailController.vttSubtitlesIndex =
|
||||
plPlayerController!.vttSubtitlesIndex.value;
|
||||
videoDetailController.defaultST = plPlayerController!.position.value;
|
||||
plPlayerController!.removeStatusLister(playerListener);
|
||||
plPlayerController!.pause();
|
||||
@@ -960,6 +965,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
),
|
||||
),
|
||||
showEpisodes: showEpisodes,
|
||||
showViewPoints: showViewPoints,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1058,6 +1064,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
),
|
||||
),
|
||||
showEpisodes: showEpisodes,
|
||||
showViewPoints: showViewPoints,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
@@ -1379,8 +1386,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
}
|
||||
|
||||
showEpisodes(index, season, episodes, bvid, aid, cid) {
|
||||
PersistentBottomSheetController? bottomSheetController;
|
||||
|
||||
Widget listSheetContent() => ListSheetContent(
|
||||
index: index,
|
||||
season: season,
|
||||
@@ -1392,15 +1397,162 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
videoDetailController.videoType == SearchType.media_bangumi
|
||||
? bangumiIntroController.changeSeasonOrbangu
|
||||
: videoIntroController.changeSeasonOrbangu,
|
||||
onClose: bottomSheetController?.close,
|
||||
);
|
||||
if (isFullScreen) {
|
||||
videoDetailController.scaffoldKey.currentState?.showBottomSheet(
|
||||
(context) => listSheetContent(),
|
||||
);
|
||||
} else {
|
||||
videoDetailController.childKey.currentState?.showBottomSheet(
|
||||
(context) => listSheetContent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bottomSheetController = isFullScreen
|
||||
? videoDetailController.scaffoldKey.currentState?.showBottomSheet(
|
||||
(context) => listSheetContent(),
|
||||
)
|
||||
: videoDetailController.scaffoldKey.currentState?.showBottomSheet(
|
||||
(context) => listSheetContent(),
|
||||
);
|
||||
void showViewPoints() {
|
||||
Widget listSheetContent(context, [bool isFS = false]) {
|
||||
int currentIndex = -1;
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) => SizedBox(
|
||||
height: isFS ? Utils.getSheetHeight(context) : null,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: 16,
|
||||
title: 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,
|
||||
(index) {
|
||||
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!));
|
||||
setState(() {});
|
||||
}
|
||||
: 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: (_, 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) {
|
||||
videoDetailController.scaffoldKey.currentState?.showBottomSheet(
|
||||
(context) => listSheetContent(context, true),
|
||||
);
|
||||
} else {
|
||||
videoDetailController.childKey.currentState?.showBottomSheet(
|
||||
(context) => listSheetContent(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -894,6 +894,8 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
double fontSizeVal = widget.controller!.fontSizeVal;
|
||||
// 全屏字体大小
|
||||
double fontSizeFSVal = widget.controller!.fontSizeFSVal;
|
||||
double subtitleFontScale = widget.controller!.subtitleFontScale.value;
|
||||
double subtitleFontScaleFS = widget.controller!.subtitleFontScaleFS.value;
|
||||
// 弹幕速度
|
||||
double danmakuDurationVal = widget.controller!.danmakuDurationVal;
|
||||
// 弹幕描边
|
||||
@@ -912,13 +914,18 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 580,
|
||||
height: 600,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
margin: const EdgeInsets.all(12),
|
||||
margin: EdgeInsets.only(
|
||||
left: 12,
|
||||
top: 12,
|
||||
right: 12,
|
||||
bottom: widget.controller?.isFullScreen.value == true ? 70 : 12,
|
||||
),
|
||||
padding: const EdgeInsets.only(left: 14, right: 14),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
@@ -1258,6 +1265,76 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'字幕字体大小 ${(subtitleFontScale * 100).toStringAsFixed(1)}%'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 0,
|
||||
bottom: 6,
|
||||
left: 10,
|
||||
right: 10,
|
||||
),
|
||||
child: SliderTheme(
|
||||
data: SliderThemeData(
|
||||
trackShape: MSliderTrackShape(),
|
||||
thumbColor: Theme.of(context).colorScheme.primary,
|
||||
activeTrackColor: Theme.of(context).colorScheme.primary,
|
||||
trackHeight: 10,
|
||||
thumbShape: const RoundSliderThumbShape(
|
||||
enabledThumbRadius: 6.0),
|
||||
),
|
||||
child: Slider(
|
||||
min: 0.5,
|
||||
max: 2.5,
|
||||
value: subtitleFontScale,
|
||||
divisions: 20,
|
||||
label:
|
||||
'${(subtitleFontScale * 100).toStringAsFixed(1)}%',
|
||||
onChanged: (double val) {
|
||||
subtitleFontScale = val;
|
||||
widget.controller!.subtitleFontScale.value =
|
||||
subtitleFontScale;
|
||||
widget.controller?.putDanmakuSettings();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'全屏字幕字体大小 ${(subtitleFontScaleFS * 100).toStringAsFixed(1)}%'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 0,
|
||||
bottom: 6,
|
||||
left: 10,
|
||||
right: 10,
|
||||
),
|
||||
child: SliderTheme(
|
||||
data: SliderThemeData(
|
||||
trackShape: MSliderTrackShape(),
|
||||
thumbColor: Theme.of(context).colorScheme.primary,
|
||||
activeTrackColor: Theme.of(context).colorScheme.primary,
|
||||
trackHeight: 10,
|
||||
thumbShape: const RoundSliderThumbShape(
|
||||
enabledThumbRadius: 6.0),
|
||||
),
|
||||
child: Slider(
|
||||
min: 0.5,
|
||||
max: 2.5,
|
||||
value: subtitleFontScaleFS,
|
||||
divisions: 20,
|
||||
label:
|
||||
'${(subtitleFontScaleFS * 100).toStringAsFixed(1)}%',
|
||||
onChanged: (double val) {
|
||||
subtitleFontScaleFS = val;
|
||||
widget.controller!.subtitleFontScaleFS.value =
|
||||
subtitleFontScaleFS;
|
||||
widget.controller?.putDanmakuSettings();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Text('弹幕时长 $danmakuDurationVal 秒'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
|
||||
@@ -8,8 +8,17 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
// ignore: constant_identifier_names
|
||||
enum WebviewMenuItem { Refresh, Copy, Open_In_Browser, Clear_Cache, Go_Back }
|
||||
enum WebviewMenuItem { refresh, copy, openInBrowser, clearCache, goBack }
|
||||
|
||||
extension WebviewMenuItemExt on WebviewMenuItem {
|
||||
String get title => [
|
||||
'刷新',
|
||||
'复制链接',
|
||||
'浏览器中打开',
|
||||
'清除缓存',
|
||||
'返回',
|
||||
][index];
|
||||
}
|
||||
|
||||
class WebviewPageNew extends StatefulWidget {
|
||||
const WebviewPageNew({super.key});
|
||||
@@ -63,22 +72,22 @@ class _WebviewPageNewState extends State<WebviewPageNew> {
|
||||
PopupMenuButton(
|
||||
onSelected: (item) async {
|
||||
switch (item) {
|
||||
case WebviewMenuItem.Refresh:
|
||||
case WebviewMenuItem.refresh:
|
||||
_webViewController?.reload();
|
||||
break;
|
||||
case WebviewMenuItem.Copy:
|
||||
case WebviewMenuItem.copy:
|
||||
WebUri? uri = await _webViewController?.getUrl();
|
||||
if (uri != null) {
|
||||
Utils.copyText(uri.toString());
|
||||
}
|
||||
break;
|
||||
case WebviewMenuItem.Open_In_Browser:
|
||||
case WebviewMenuItem.openInBrowser:
|
||||
WebUri? uri = await _webViewController?.getUrl();
|
||||
if (uri != null) {
|
||||
Utils.launchURL(uri.toString());
|
||||
}
|
||||
break;
|
||||
case WebviewMenuItem.Clear_Cache:
|
||||
case WebviewMenuItem.clearCache:
|
||||
try {
|
||||
await InAppWebViewController.clearAllCache();
|
||||
await _webViewController?.clearHistory();
|
||||
@@ -87,21 +96,23 @@ class _WebviewPageNewState extends State<WebviewPageNew> {
|
||||
SmartDialog.showToast(e.toString());
|
||||
}
|
||||
break;
|
||||
case WebviewMenuItem.Go_Back:
|
||||
case WebviewMenuItem.goBack:
|
||||
if (await _webViewController?.canGoBack() == true) {
|
||||
_webViewController?.goBack();
|
||||
} else {
|
||||
Get.back();
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => <PopupMenuEntry<WebviewMenuItem>>[
|
||||
...WebviewMenuItem.values.sublist(0, 4).map(
|
||||
(item) => PopupMenuItem(value: item, child: Text(item.name))),
|
||||
...WebviewMenuItem.values.sublist(0, 4).map((item) =>
|
||||
PopupMenuItem(value: item, child: Text(item.title))),
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
value: WebviewMenuItem.Go_Back,
|
||||
value: WebviewMenuItem.goBack,
|
||||
child: Text(
|
||||
WebviewMenuItem.Go_Back.name,
|
||||
WebviewMenuItem.goBack.title,
|
||||
style:
|
||||
TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
)),
|
||||
|
||||
@@ -25,8 +25,6 @@ import 'package:PiliPalaX/utils/storage.dart';
|
||||
import 'package:screen_brightness/screen_brightness.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
import '../../models/video/play/subtitle.dart';
|
||||
|
||||
Box videoStorage = GStorage.video;
|
||||
Box setting = GStorage.setting;
|
||||
Box localCache = GStorage.localCache;
|
||||
@@ -104,8 +102,9 @@ class PlPlayerController {
|
||||
bool _enableHeart = true;
|
||||
|
||||
late DataSource dataSource;
|
||||
final RxList<Map<String, String>> _vttSubtitles = <Map<String, String>>[].obs;
|
||||
final RxInt _vttSubtitlesIndex = 0.obs;
|
||||
// 视频字幕
|
||||
final RxList<Map<String, String>> vttSubtitles = <Map<String, String>>[].obs;
|
||||
final RxInt vttSubtitlesIndex = 0.obs;
|
||||
|
||||
Timer? _timer;
|
||||
Timer? _timerForSeek;
|
||||
@@ -114,6 +113,8 @@ class PlPlayerController {
|
||||
Timer? _timerForGettingVolume;
|
||||
Timer? timerForTrackingMouse;
|
||||
|
||||
final RxList<Segment> viewPointList = <Segment>[].obs;
|
||||
final RxBool showVP = true.obs;
|
||||
final RxList<Segment> segmentList = <Segment>[].obs;
|
||||
|
||||
// final Durations durations;
|
||||
@@ -163,10 +164,6 @@ class PlPlayerController {
|
||||
Rx<bool> get mute => _mute;
|
||||
Stream<bool> get onMuteChanged => _mute.stream;
|
||||
|
||||
// 视频字幕
|
||||
RxList<Map<String, String>> get vttSubtitles => _vttSubtitles;
|
||||
RxInt get vttSubtitlesIndex => _vttSubtitlesIndex;
|
||||
|
||||
/// [videoPlayerController] instance of Player
|
||||
Player? get videoPlayerController => _videoPlayerController;
|
||||
|
||||
@@ -258,6 +255,8 @@ class PlPlayerController {
|
||||
double? defaultDuration;
|
||||
late bool enableAutoLongPressSpeed = false;
|
||||
late bool enableLongShowControl;
|
||||
RxDouble subtitleFontScale = (1.0).obs;
|
||||
RxDouble subtitleFontScaleFS = (1.5).obs;
|
||||
|
||||
// 播放顺序相关
|
||||
PlayRepeat playRepeat = PlayRepeat.pause;
|
||||
@@ -351,6 +350,8 @@ class PlPlayerController {
|
||||
setting.get(SettingBoxKey.danmakuFontScale, defaultValue: 1.0);
|
||||
// 全屏字体大小
|
||||
fontSizeFSVal = GStorage.danmakuFontScaleFS;
|
||||
subtitleFontScale.value = GStorage.subtitleFontScale;
|
||||
subtitleFontScaleFS.value = GStorage.subtitleFontScaleFS;
|
||||
// 弹幕时间
|
||||
danmakuDurationVal =
|
||||
setting.get(SettingBoxKey.danmakuDuration, defaultValue: 7.29);
|
||||
@@ -403,6 +404,9 @@ class PlPlayerController {
|
||||
Future<void> setDataSource(
|
||||
DataSource dataSource, {
|
||||
List<Segment>? segmentList,
|
||||
List<Segment>? viewPointList,
|
||||
List<Map<String, String>>? vttSubtitles,
|
||||
int? vttSubtitlesIndex,
|
||||
bool autoplay = true,
|
||||
// 默认不循环
|
||||
PlaylistMode looping = PlaylistMode.none,
|
||||
@@ -427,6 +431,9 @@ class PlPlayerController {
|
||||
try {
|
||||
this.dataSource = dataSource;
|
||||
this.segmentList.value = segmentList ?? <Segment>[];
|
||||
this.viewPointList.value = viewPointList ?? <Segment>[];
|
||||
this.vttSubtitles.value = vttSubtitles ?? <Map<String, String>>[];
|
||||
this.vttSubtitlesIndex.value = vttSubtitlesIndex ?? 0;
|
||||
_autoPlay = autoplay;
|
||||
_looping = looping;
|
||||
// 初始化视频倍速
|
||||
@@ -461,35 +468,7 @@ class PlPlayerController {
|
||||
startListeners();
|
||||
}
|
||||
await _initializePlayer(seekTo: seekTo);
|
||||
if (videoType.value != 'live' && _cid != 0) {
|
||||
refreshSubtitles().then((value) {
|
||||
if (_vttSubtitles.isNotEmpty) {
|
||||
if (_vttSubtitlesIndex > 0 &&
|
||||
_vttSubtitlesIndex < _vttSubtitles.length) {
|
||||
setSubtitle(_vttSubtitlesIndex.value);
|
||||
} else {
|
||||
String preference = setting.get(SettingBoxKey.subtitlePreference,
|
||||
defaultValue: SubtitlePreference.values.first.code);
|
||||
if (preference == 'on') {
|
||||
setSubtitle(1);
|
||||
} else if (preference == 'withoutAi') {
|
||||
bool found = false;
|
||||
for (int i = 1; i < _vttSubtitles.length; i++) {
|
||||
if (_vttSubtitles[i]['language']!.startsWith('ai')) {
|
||||
continue;
|
||||
}
|
||||
found = true;
|
||||
setSubtitle(i);
|
||||
break;
|
||||
}
|
||||
if (!found) _vttSubtitlesIndex.value = 0;
|
||||
} else {
|
||||
_vttSubtitlesIndex.value = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
setSubtitle(this.vttSubtitlesIndex.value);
|
||||
} catch (err, stackTrace) {
|
||||
dataStatus.status.value = DataStatus.error;
|
||||
debugPrint(stackTrace.toString());
|
||||
@@ -1296,6 +1275,8 @@ class PlPlayerController {
|
||||
setting.put(SettingBoxKey.danmakuOpacity, opacityVal);
|
||||
setting.put(SettingBoxKey.danmakuFontScale, fontSizeVal);
|
||||
setting.put(SettingBoxKey.danmakuFontScaleFS, fontSizeFSVal);
|
||||
setting.put(SettingBoxKey.subtitleFontScale, subtitleFontScale.value);
|
||||
setting.put(SettingBoxKey.subtitleFontScaleFS, subtitleFontScaleFS.value);
|
||||
setting.put(SettingBoxKey.danmakuDuration, danmakuDurationVal);
|
||||
setting.put(SettingBoxKey.strokeWidth, strokeWidth);
|
||||
setting.put(SettingBoxKey.fontWeight, fontWeight);
|
||||
@@ -1345,39 +1326,20 @@ class PlPlayerController {
|
||||
}
|
||||
}
|
||||
|
||||
Future refreshSubtitles() async {
|
||||
_vttSubtitles.clear();
|
||||
Map res = await VideoHttp.subtitlesJson(bvid: _bvid, cid: _cid);
|
||||
// if (!res["status"]) {
|
||||
// SmartDialog.showToast('查询字幕错误,${res["msg"]}');
|
||||
// }
|
||||
if (res["data"].length == 0) {
|
||||
return;
|
||||
}
|
||||
var result = await VideoHttp.vttSubtitles(res["data"]);
|
||||
if (result != null) {
|
||||
_vttSubtitles.value = result;
|
||||
}
|
||||
// if (_vttSubtitles.isEmpty) {
|
||||
// SmartDialog.showToast('字幕均加载失败');
|
||||
// }
|
||||
return;
|
||||
}
|
||||
|
||||
// 设定字幕轨道
|
||||
setSubtitle(int index) {
|
||||
if (index == 0) {
|
||||
_videoPlayerController?.setSubtitleTrack(SubtitleTrack.no());
|
||||
_vttSubtitlesIndex.value = 0;
|
||||
vttSubtitlesIndex.value = 0;
|
||||
return;
|
||||
}
|
||||
Map<String, String> s = _vttSubtitles[index];
|
||||
Map<String, String> s = vttSubtitles[index];
|
||||
_videoPlayerController?.setSubtitleTrack(SubtitleTrack.data(
|
||||
s['text']!,
|
||||
title: s['title']!,
|
||||
language: s['language']!,
|
||||
));
|
||||
_vttSubtitlesIndex.value = index;
|
||||
vttSubtitlesIndex.value = index;
|
||||
}
|
||||
|
||||
static void updatePlayCount() {
|
||||
|
||||
@@ -10,4 +10,5 @@ enum BottomControlType {
|
||||
speed,
|
||||
fullscreen,
|
||||
custom,
|
||||
viewPoints,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:PiliPalaX/common/widgets/segment_progress_bar.dart';
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
@@ -47,6 +48,7 @@ class PLVideoPlayer extends StatefulWidget {
|
||||
this.customWidget,
|
||||
this.customWidgets,
|
||||
this.showEpisodes,
|
||||
this.showViewPoints,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -62,6 +64,7 @@ class PLVideoPlayer extends StatefulWidget {
|
||||
final Widget? customWidget;
|
||||
final List<Widget>? customWidgets;
|
||||
final Function? showEpisodes;
|
||||
final VoidCallback? showViewPoints;
|
||||
|
||||
@override
|
||||
State<PLVideoPlayer> createState() => _PLVideoPlayerState();
|
||||
@@ -75,6 +78,8 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
late BangumiIntroController? bangumiIntroController;
|
||||
|
||||
final GlobalKey _playerKey = GlobalKey();
|
||||
final GlobalKey<VideoState> _key = GlobalKey<VideoState>();
|
||||
|
||||
final RxBool _mountSeekBackwardButton = false.obs;
|
||||
final RxBool _mountSeekForwardButton = false.obs;
|
||||
final RxBool _hideSeekBackwardButton = false.obs;
|
||||
@@ -234,7 +239,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
Map<BottomControlType, Widget> videoProgressWidgets = {
|
||||
/// 上一集
|
||||
BottomControlType.pre: Container(
|
||||
width: 42,
|
||||
width: 35,
|
||||
height: 30,
|
||||
alignment: Alignment.center,
|
||||
child: ComBtn(
|
||||
@@ -266,7 +271,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
|
||||
/// 下一集
|
||||
BottomControlType.next: Container(
|
||||
width: 42,
|
||||
width: 35,
|
||||
height: 30,
|
||||
alignment: Alignment.center,
|
||||
child: ComBtn(
|
||||
@@ -328,9 +333,32 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
/// 空白占位
|
||||
BottomControlType.space: const Spacer(),
|
||||
|
||||
/// 分段信息
|
||||
BottomControlType.viewPoints: Obx(
|
||||
() => plPlayerController.viewPointList.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: Container(
|
||||
width: 35,
|
||||
height: 30,
|
||||
alignment: Alignment.center,
|
||||
child: ComBtn(
|
||||
icon: Transform.rotate(
|
||||
angle: pi / 2,
|
||||
child: const Icon(
|
||||
Icons.reorder,
|
||||
semanticLabel: '分段信息',
|
||||
size: 22,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
fuc: widget.showViewPoints,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
/// 选集
|
||||
BottomControlType.episode: Container(
|
||||
width: 42,
|
||||
width: 35,
|
||||
height: 30,
|
||||
alignment: Alignment.center,
|
||||
child: ComBtn(
|
||||
@@ -385,7 +413,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
|
||||
/// 画面比例
|
||||
BottomControlType.fit: SizedBox(
|
||||
width: 42,
|
||||
width: 35,
|
||||
height: 30,
|
||||
child: TextButton(
|
||||
onPressed: () => plPlayerController.toggleVideoFit(),
|
||||
@@ -406,7 +434,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
() => plPlayerController.vttSubtitles.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: SizedBox(
|
||||
width: 42,
|
||||
width: 35,
|
||||
height: 30,
|
||||
child: PopupMenuButton<int>(
|
||||
onSelected: (int value) {
|
||||
@@ -432,7 +460,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
}).toList();
|
||||
},
|
||||
child: Container(
|
||||
width: 42,
|
||||
width: 35,
|
||||
height: 30,
|
||||
alignment: Alignment.center,
|
||||
child: const Icon(
|
||||
@@ -448,7 +476,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
|
||||
/// 播放速度
|
||||
BottomControlType.speed: SizedBox(
|
||||
width: 42,
|
||||
width: 35,
|
||||
height: 30,
|
||||
child: PopupMenuButton<double>(
|
||||
onSelected: (double value) {
|
||||
@@ -471,7 +499,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
}).toList();
|
||||
},
|
||||
child: Container(
|
||||
width: 42,
|
||||
width: 35,
|
||||
height: 30,
|
||||
alignment: Alignment.center,
|
||||
child: Obx(() => Text("${plPlayerController.playbackSpeed}X",
|
||||
@@ -483,7 +511,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
|
||||
/// 全屏
|
||||
BottomControlType.fullscreen: SizedBox(
|
||||
width: 42,
|
||||
width: 35,
|
||||
height: 30,
|
||||
child: Obx(() => ComBtn(
|
||||
icon: Icon(
|
||||
@@ -508,6 +536,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
if (anySeason) BottomControlType.pre,
|
||||
if (anySeason) BottomControlType.next,
|
||||
BottomControlType.space,
|
||||
BottomControlType.viewPoints,
|
||||
if (anySeason) BottomControlType.episode,
|
||||
if (plPlayerController.isFullScreen.value) BottomControlType.fit,
|
||||
BottomControlType.subtitle,
|
||||
@@ -529,19 +558,43 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
return list;
|
||||
}
|
||||
|
||||
PlPlayerController get plPlayerController => widget.controller;
|
||||
|
||||
TextStyle get subTitleStyle => TextStyle(
|
||||
height: 1.5,
|
||||
fontSize: 16 *
|
||||
(plPlayerController.isFullScreen.value
|
||||
? plPlayerController.subtitleFontScaleFS.value
|
||||
: plPlayerController.subtitleFontScale.value),
|
||||
letterSpacing: 0.1,
|
||||
wordSpacing: 0.1,
|
||||
color: Color(0xffffffff),
|
||||
fontWeight: FontWeight.normal,
|
||||
backgroundColor: Color(0xaa000000),
|
||||
);
|
||||
|
||||
void _updateSubtitle(double value) {
|
||||
_key.currentState?.update(
|
||||
subtitleViewConfiguration: SubtitleViewConfiguration(
|
||||
style: subTitleStyle.copyWith(fontSize: 16 * value),
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final PlPlayerController plPlayerController = widget.controller;
|
||||
if (plPlayerController.isFullScreen.value) {
|
||||
plPlayerController.subtitleFontScaleFS.listen((value) {
|
||||
_updateSubtitle(value);
|
||||
});
|
||||
} else {
|
||||
plPlayerController.subtitleFontScale.listen((value) {
|
||||
_updateSubtitle(value);
|
||||
});
|
||||
}
|
||||
final Color colorTheme = Theme.of(context).colorScheme.primary;
|
||||
const TextStyle subTitleStyle = TextStyle(
|
||||
height: 1.5,
|
||||
fontSize: 20.0,
|
||||
letterSpacing: 0.1,
|
||||
wordSpacing: 0.1,
|
||||
color: Color(0xffffffff),
|
||||
fontWeight: FontWeight.normal,
|
||||
backgroundColor: Color(0xaa000000),
|
||||
);
|
||||
const TextStyle textStyle = TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
@@ -684,7 +737,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
_gestureType = null;
|
||||
},
|
||||
child: Video(
|
||||
key: ValueKey('${plPlayerController.videoFit.value}'),
|
||||
key: _key,
|
||||
controller: videoController,
|
||||
controls: NoVideoControls,
|
||||
pauseUponEnteringBackgroundMode:
|
||||
@@ -988,7 +1041,6 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
BottomControl(
|
||||
controller: widget.controller,
|
||||
buildBottomControl: buildBottomControl(),
|
||||
segmentList: plPlayerController.segmentList,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1089,6 +1141,15 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
segmentColors: plPlayerController.segmentList,
|
||||
),
|
||||
),
|
||||
if (plPlayerController.viewPointList.isNotEmpty &&
|
||||
plPlayerController.showVP.value)
|
||||
CustomPaint(
|
||||
size: Size(double.infinity, 3.5),
|
||||
painter: SegmentProgressBar(
|
||||
progress: 1,
|
||||
segmentColors: plPlayerController.viewPointList,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// SlideTransition(
|
||||
|
||||
@@ -13,11 +13,9 @@ import '../../../common/widgets/audio_video_progress_bar.dart';
|
||||
class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
||||
final PlPlayerController? controller;
|
||||
final List<Widget>? buildBottomControl;
|
||||
final List<Segment>? segmentList;
|
||||
const BottomControl({
|
||||
this.controller,
|
||||
this.buildBottomControl,
|
||||
this.segmentList,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -98,12 +96,21 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
||||
TextDirection.ltr);
|
||||
},
|
||||
),
|
||||
if (segmentList?.isNotEmpty == true)
|
||||
if (controller?.segmentList.isNotEmpty == true)
|
||||
CustomPaint(
|
||||
size: Size(double.infinity, 3.5),
|
||||
painter: SegmentProgressBar(
|
||||
progress: 1,
|
||||
segmentColors: segmentList!,
|
||||
segmentColors: controller!.segmentList,
|
||||
),
|
||||
),
|
||||
if (controller?.viewPointList.isNotEmpty == true &&
|
||||
controller?.showVP.value == true)
|
||||
CustomPaint(
|
||||
size: Size(double.infinity, 3.5),
|
||||
painter: SegmentProgressBar(
|
||||
progress: 1,
|
||||
segmentColors: controller!.viewPointList,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -99,9 +99,29 @@ class GStorage {
|
||||
static double get danmakuFontScaleFS =>
|
||||
setting.get(SettingBoxKey.danmakuFontScaleFS, defaultValue: 1.2);
|
||||
|
||||
static double get subtitleFontScale =>
|
||||
setting.get(SettingBoxKey.subtitleFontScale, defaultValue: 1.0);
|
||||
|
||||
static double get subtitleFontScaleFS =>
|
||||
setting.get(SettingBoxKey.subtitleFontScaleFS, defaultValue: 1.5);
|
||||
|
||||
static bool get grpcReply =>
|
||||
setting.get(SettingBoxKey.grpcReply, defaultValue: true);
|
||||
|
||||
static bool get showViewPoints =>
|
||||
setting.get(SettingBoxKey.showViewPoints, defaultValue: true);
|
||||
|
||||
static List<double> get dynamicDetailRatio =>
|
||||
setting.get(SettingBoxKey.dynamicDetailRatio, defaultValue: [60.0, 40.0]);
|
||||
|
||||
static List<int> get blackMidsList => List<int>.from(GStorage.localCache
|
||||
.get(LocalCacheKey.blackMidsList, defaultValue: <int>[]));
|
||||
|
||||
static void setBlackMidsList(blackMidsList) {
|
||||
if (blackMidsList is! List<int>) return;
|
||||
GStorage.localCache.put(LocalCacheKey.blackMidsList, blackMidsList);
|
||||
}
|
||||
|
||||
static MemberTabType get memberTab => MemberTabType
|
||||
.values[setting.get(SettingBoxKey.memberTab, defaultValue: 0)];
|
||||
|
||||
@@ -280,6 +300,7 @@ class SettingBoxKey {
|
||||
dynamicPeriod = 'dynamicPeriod',
|
||||
schemeVariant = 'schemeVariant',
|
||||
grpcReply = 'grpcReply',
|
||||
showViewPoints = 'showViewPoints',
|
||||
|
||||
// Sponsor Block
|
||||
enableSponsorBlock = 'enableSponsorBlock',
|
||||
@@ -302,6 +323,9 @@ class SettingBoxKey {
|
||||
strokeWidth = 'strokeWidth',
|
||||
fontWeight = 'fontWeight',
|
||||
memberTab = 'memberTab',
|
||||
subtitleFontScale = 'subtitleFontScale',
|
||||
subtitleFontScaleFS = 'subtitleFontScaleFS',
|
||||
dynamicDetailRatio = 'dynamicDetailRatio',
|
||||
|
||||
// 代理host port
|
||||
systemProxyHost = 'systemProxyHost',
|
||||
|
||||
Reference in New Issue
Block a user