Compare commits

..

13 Commits

Author SHA1 Message Date
bggRGjQaUbCoE
3d7583e010 fix: reset subtitle, viewpoints
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2024-12-02 15:26:05 +08:00
bggRGjQaUbCoE
64ff4e0d5c fix: fav search item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2024-12-02 15:01:27 +08:00
bggRGjQaUbCoE
84ee106ddf opt: blackMidsList
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2024-12-02 13:57:48 +08:00
bggRGjQaUbCoE
cbdd8e77db opt: video subtitle
avoid refetching subtitle
fix stuck when parsing large subtitle body

opt: viewpoints

Update README.md

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2024-12-02 13:48:43 +08:00
bggRGjQaUbCoE
a0b1e23727 opt: viewpoints
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2024-12-01 19:40:08 +08:00
bggRGjQaUbCoE
aa05ae3f32 fix: refresh member video
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2024-12-01 18:29:25 +08:00
bggRGjQaUbCoE
7d7ae3f130 opt: viewpoints
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2024-12-01 17:38:30 +08:00
bggRGjQaUbCoE
f9ed31c65a feat: progressbar: show viewpoints #28
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2024-12-01 16:18:57 +08:00
bggRGjQaUbCoE
43977c737b fix: loadingState cast
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2024-12-01 13:59:02 +08:00
bggRGjQaUbCoE
62a1768307 fix: refresh rcmd
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2024-12-01 13:16:09 +08:00
bggRGjQaUbCoE
26e8553d9e opt: dynamic detail/html page
Closes #26

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2024-12-01 12:39:13 +08:00
bggRGjQaUbCoE
018424d5bd feat: custom subtitle fontscale
Closes #28

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2024-12-01 10:37:02 +08:00
bggRGjQaUbCoE
a6f5bd8d7d opt: action item gesture
Closes #29

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2024-12-01 09:04:22 +08:00
35 changed files with 853 additions and 262 deletions

View File

@@ -47,6 +47,10 @@
## feat
- [x] 显示视频分段信息
- [x] 调节字幕大小
- [x] 调节全屏弹幕大小
- [x] 收藏夹/稍后再看多选删除
- [x] 搜索用户动态
- [x] 直播弹幕
- [x] 修改头像/用户名/签名/性别/生日

View File

@@ -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,

View File

@@ -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),
),

View File

@@ -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,
),
],
),

View File

@@ -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;
}
}

View File

@@ -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'] ?? '成功');
},

View File

@@ -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']) {
// 屏蔽推广和拉黑用户

View File

@@ -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));

View File

@@ -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(

View File

@@ -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 {

View File

@@ -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']);
}
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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),
),
),
],
),
),
],
);

View File

@@ -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,

View File

@@ -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('取消收藏');

View File

@@ -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('确认'),
)

View File

@@ -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;

View File

@@ -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),

View File

@@ -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;

View File

@@ -29,7 +29,8 @@ class MemberVideoCtr extends CommonController {
aid = null;
next = null;
currentPage = 0;
return super.onRefresh();
isEnd = false;
await queryData();
}
@override

View File

@@ -43,8 +43,9 @@ class RcmdController extends PopupController {
}
@override
Future onRefresh() {
Future onRefresh() async {
currentPage = 0;
return super.onRefresh();
isEnd = false;
await queryData();
}
}

View File

@@ -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,

View File

@@ -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('重启生效');
}

View File

@@ -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;
}
}
}
}

View File

@@ -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();

View File

@@ -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 =

View File

@@ -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),
);
}
}
}

View File

@@ -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(

View File

@@ -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),
)),

View File

@@ -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() {

View File

@@ -10,4 +10,5 @@ enum BottomControlType {
speed,
fullscreen,
custom,
viewPoints,
}

View File

@@ -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(

View File

@@ -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,
),
),
],

View File

@@ -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',