Compare commits
35 Commits
feature-up
...
feature-me
| Author | SHA1 | Date | |
|---|---|---|---|
| b689db16b5 | |||
| 5cb3e578a8 | |||
| 9845f0383a | |||
| fb3be848b4 | |||
| 7d7df17317 | |||
| aae08d0688 | |||
| 9fe5b78cfa | |||
| 6b028c36af | |||
| 92c385ff58 | |||
| 463ee1d5b5 | |||
| 0a416c95bc | |||
| 3d09d80007 | |||
| fffa15faa3 | |||
| d6fd299395 | |||
| 1953653044 | |||
| 9faa625d52 | |||
| 955d8f5401 | |||
| 1f75a7e781 | |||
| 2cd8ab7d27 | |||
| 7e7bb1f43a | |||
| a925ef63eb | |||
| 862ccea879 | |||
| 031d57e1fd | |||
| 77b509fd17 | |||
| 4db5a950f3 | |||
| c9327c97e5 | |||
| 8ff387d54a | |||
| 0f0546ae59 | |||
| 76784ee664 | |||
| 2dbef3fee2 | |||
| 3a66c8c03d | |||
| 42ed67e03f | |||
| 1eb2d23fb9 | |||
| ee368d348d | |||
| 6f62837495 |
@ -484,6 +484,9 @@ class Api {
|
|||||||
/// 激活buvid3
|
/// 激活buvid3
|
||||||
static const activateBuvidApi = '/x/internal/gaia-gateway/ExClimbWuzhi';
|
static const activateBuvidApi = '/x/internal/gaia-gateway/ExClimbWuzhi';
|
||||||
|
|
||||||
|
/// 获取字幕配置
|
||||||
|
static const getSubtitleConfig = '/x/player/v2';
|
||||||
|
|
||||||
/// 我的订阅
|
/// 我的订阅
|
||||||
static const userSubFolder = '/x/v3/fav/folder/collected/list';
|
static const userSubFolder = '/x/v3/fav/folder/collected/list';
|
||||||
|
|
||||||
|
|||||||
@ -8,9 +8,11 @@ import '../models/model_rec_video_item.dart';
|
|||||||
import '../models/user/fav_folder.dart';
|
import '../models/user/fav_folder.dart';
|
||||||
import '../models/video/ai.dart';
|
import '../models/video/ai.dart';
|
||||||
import '../models/video/play/url.dart';
|
import '../models/video/play/url.dart';
|
||||||
|
import '../models/video/subTitile/result.dart';
|
||||||
import '../models/video_detail_res.dart';
|
import '../models/video_detail_res.dart';
|
||||||
import '../utils/recommend_filter.dart';
|
import '../utils/recommend_filter.dart';
|
||||||
import '../utils/storage.dart';
|
import '../utils/storage.dart';
|
||||||
|
import '../utils/subtitle.dart';
|
||||||
import '../utils/wbi_sign.dart';
|
import '../utils/wbi_sign.dart';
|
||||||
import 'api.dart';
|
import 'api.dart';
|
||||||
import 'init.dart';
|
import 'init.dart';
|
||||||
@ -476,6 +478,25 @@ class VideoHttp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future getSubtitle({int? cid, String? bvid}) async {
|
||||||
|
var res = await Request().get(Api.getSubtitleConfig, data: {
|
||||||
|
'cid': cid,
|
||||||
|
'bvid': bvid,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': SubTitlteModel.fromJson(res.data['data']),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {'status': false, 'data': [], 'msg': res.data['msg']};
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
print(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 视频排行
|
// 视频排行
|
||||||
static Future getRankVideoList(int rid) async {
|
static Future getRankVideoList(int rid) async {
|
||||||
try {
|
try {
|
||||||
@ -498,4 +519,12 @@ class VideoHttp {
|
|||||||
return {'status': false, 'data': [], 'msg': err};
|
return {'status': false, 'data': [], 'msg': err};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取字幕内容
|
||||||
|
static Future<Map<String, dynamic>> getSubtitleContent(url) async {
|
||||||
|
var res = await Request().get('https:$url');
|
||||||
|
final String content = SubTitleUtils.convertToWebVTT(res.data['body']);
|
||||||
|
final List body = res.data['body'];
|
||||||
|
return {'content': content, 'body': body};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
47
lib/models/common/subtitle_type.dart
Normal file
47
lib/models/common/subtitle_type.dart
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
enum SubtitleType {
|
||||||
|
// 中文(中国)
|
||||||
|
zhCN,
|
||||||
|
// 中文(自动翻译)
|
||||||
|
aizh,
|
||||||
|
// 英语(自动生成)
|
||||||
|
aien,
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SubtitleTypeExtension on SubtitleType {
|
||||||
|
String get description {
|
||||||
|
switch (this) {
|
||||||
|
case SubtitleType.zhCN:
|
||||||
|
return '中文(中国)';
|
||||||
|
case SubtitleType.aizh:
|
||||||
|
return '中文(自动翻译)';
|
||||||
|
case SubtitleType.aien:
|
||||||
|
return '英语(自动生成)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SubtitleIdExtension on SubtitleType {
|
||||||
|
String get id {
|
||||||
|
switch (this) {
|
||||||
|
case SubtitleType.zhCN:
|
||||||
|
return 'zh-CN';
|
||||||
|
case SubtitleType.aizh:
|
||||||
|
return 'ai-zh';
|
||||||
|
case SubtitleType.aien:
|
||||||
|
return 'ai-en';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SubtitleCodeExtension on SubtitleType {
|
||||||
|
int get code {
|
||||||
|
switch (this) {
|
||||||
|
case SubtitleType.zhCN:
|
||||||
|
return 1;
|
||||||
|
case SubtitleType.aizh:
|
||||||
|
return 2;
|
||||||
|
case SubtitleType.aien:
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -437,7 +437,8 @@ class SearchArticleItemModel {
|
|||||||
pubTime = json['pub_time'];
|
pubTime = json['pub_time'];
|
||||||
like = json['like'];
|
like = json['like'];
|
||||||
title = Em.regTitle(json['title']);
|
title = Em.regTitle(json['title']);
|
||||||
subTitle = json['title'].replaceAll(RegExp(r'<[^>]*>'), '');
|
subTitle =
|
||||||
|
Em.decodeHtmlEntities(json['title'].replaceAll(RegExp(r'<[^>]*>'), ''));
|
||||||
rankOffset = json['rank_offset'];
|
rankOffset = json['rank_offset'];
|
||||||
mid = json['mid'];
|
mid = json['mid'];
|
||||||
imageUrls = json['image_urls'];
|
imageUrls = json['image_urls'];
|
||||||
|
|||||||
20
lib/models/video/subTitile/content.dart
Normal file
20
lib/models/video/subTitile/content.dart
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
class SubTitileContentModel {
|
||||||
|
double? from;
|
||||||
|
double? to;
|
||||||
|
int? location;
|
||||||
|
String? content;
|
||||||
|
|
||||||
|
SubTitileContentModel({
|
||||||
|
this.from,
|
||||||
|
this.to,
|
||||||
|
this.location,
|
||||||
|
this.content,
|
||||||
|
});
|
||||||
|
|
||||||
|
SubTitileContentModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
from = json['from'];
|
||||||
|
to = json['to'];
|
||||||
|
location = json['location'];
|
||||||
|
content = json['content'];
|
||||||
|
}
|
||||||
|
}
|
||||||
89
lib/models/video/subTitile/result.dart
Normal file
89
lib/models/video/subTitile/result.dart
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../../common/subtitle_type.dart';
|
||||||
|
|
||||||
|
class SubTitlteModel {
|
||||||
|
SubTitlteModel({
|
||||||
|
this.aid,
|
||||||
|
this.bvid,
|
||||||
|
this.cid,
|
||||||
|
this.loginMid,
|
||||||
|
this.loginMidHash,
|
||||||
|
this.isOwner,
|
||||||
|
this.name,
|
||||||
|
this.subtitles,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? aid;
|
||||||
|
String? bvid;
|
||||||
|
int? cid;
|
||||||
|
int? loginMid;
|
||||||
|
String? loginMidHash;
|
||||||
|
bool? isOwner;
|
||||||
|
String? name;
|
||||||
|
List<SubTitlteItemModel>? subtitles;
|
||||||
|
|
||||||
|
factory SubTitlteModel.fromJson(Map<String, dynamic> json) => SubTitlteModel(
|
||||||
|
aid: json["aid"],
|
||||||
|
bvid: json["bvid"],
|
||||||
|
cid: json["cid"],
|
||||||
|
loginMid: json["login_mid"],
|
||||||
|
loginMidHash: json["login_mid_hash"],
|
||||||
|
isOwner: json["is_owner"],
|
||||||
|
name: json["name"],
|
||||||
|
subtitles: json["subtitle"] != null
|
||||||
|
? json["subtitle"]["subtitles"]
|
||||||
|
.map<SubTitlteItemModel>((x) => SubTitlteItemModel.fromJson(x))
|
||||||
|
.toList()
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubTitlteItemModel {
|
||||||
|
SubTitlteItemModel({
|
||||||
|
this.id,
|
||||||
|
this.lan,
|
||||||
|
this.lanDoc,
|
||||||
|
this.isLock,
|
||||||
|
this.subtitleUrl,
|
||||||
|
this.type,
|
||||||
|
this.aiType,
|
||||||
|
this.aiStatus,
|
||||||
|
this.title,
|
||||||
|
this.code,
|
||||||
|
this.content,
|
||||||
|
this.body,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? id;
|
||||||
|
String? lan;
|
||||||
|
String? lanDoc;
|
||||||
|
bool? isLock;
|
||||||
|
String? subtitleUrl;
|
||||||
|
int? type;
|
||||||
|
int? aiType;
|
||||||
|
int? aiStatus;
|
||||||
|
String? title;
|
||||||
|
int? code;
|
||||||
|
String? content;
|
||||||
|
List? body;
|
||||||
|
|
||||||
|
factory SubTitlteItemModel.fromJson(Map<String, dynamic> json) =>
|
||||||
|
SubTitlteItemModel(
|
||||||
|
id: json["id"],
|
||||||
|
lan: json["lan"].replaceAll('-', ''),
|
||||||
|
lanDoc: json["lan_doc"],
|
||||||
|
isLock: json["is_lock"],
|
||||||
|
subtitleUrl: json["subtitle_url"],
|
||||||
|
type: json["type"],
|
||||||
|
aiType: json["ai_type"],
|
||||||
|
aiStatus: json["ai_status"],
|
||||||
|
title: json["lan_doc"],
|
||||||
|
code: SubtitleType.values
|
||||||
|
.firstWhereOrNull(
|
||||||
|
(element) => element.id.toString() == json["lan"])
|
||||||
|
?.index ??
|
||||||
|
-1,
|
||||||
|
content: '',
|
||||||
|
body: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -185,7 +185,7 @@ class HistoryItem extends StatelessWidget {
|
|||||||
? '已看完'
|
? '已看完'
|
||||||
: '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}',
|
: '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}',
|
||||||
right: 6.0,
|
right: 6.0,
|
||||||
bottom: 6.0,
|
bottom: 8.0,
|
||||||
type: 'gray',
|
type: 'gray',
|
||||||
),
|
),
|
||||||
// 右上角
|
// 右上角
|
||||||
@ -258,6 +258,24 @@ class HistoryItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 3,
|
||||||
|
right: 3,
|
||||||
|
bottom: 0,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
bottomLeft:
|
||||||
|
Radius.circular(StyleString.imgRadius.x),
|
||||||
|
bottomRight:
|
||||||
|
Radius.circular(StyleString.imgRadius.x),
|
||||||
|
),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: videoItem.progress == -1
|
||||||
|
? 100
|
||||||
|
: videoItem.progress / videoItem.duration,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
VideoContent(videoItem: videoItem, ctr: ctr)
|
VideoContent(videoItem: videoItem, ctr: ctr)
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import 'package:pilipala/utils/storage.dart';
|
|||||||
class RankController extends GetxController with GetTickerProviderStateMixin {
|
class RankController extends GetxController with GetTickerProviderStateMixin {
|
||||||
bool flag = false;
|
bool flag = false;
|
||||||
late RxList tabs = [].obs;
|
late RxList tabs = [].obs;
|
||||||
RxInt initialIndex = 1.obs;
|
RxInt initialIndex = 0.obs;
|
||||||
late TabController tabController;
|
late TabController tabController;
|
||||||
late List tabsCtrList;
|
late List tabsCtrList;
|
||||||
late List<Widget> tabsPageList;
|
late List<Widget> tabsPageList;
|
||||||
@ -50,21 +50,5 @@ class RankController extends GetxController with GetTickerProviderStateMixin {
|
|||||||
length: tabs.length,
|
length: tabs.length,
|
||||||
vsync: this,
|
vsync: this,
|
||||||
);
|
);
|
||||||
// 监听 tabController 切换
|
|
||||||
if (enableGradientBg) {
|
|
||||||
tabController.animation!.addListener(() {
|
|
||||||
if (tabController.indexIsChanging) {
|
|
||||||
if (initialIndex.value != tabController.index) {
|
|
||||||
initialIndex.value = tabController.index;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
final int temp = tabController.animation!.value.round();
|
|
||||||
if (initialIndex.value != temp) {
|
|
||||||
initialIndex.value = temp;
|
|
||||||
tabController.index = initialIndex.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,15 +22,20 @@ class ZonePage extends StatefulWidget {
|
|||||||
State<ZonePage> createState() => _ZonePageState();
|
State<ZonePage> createState() => _ZonePageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ZonePageState extends State<ZonePage> {
|
class _ZonePageState extends State<ZonePage>
|
||||||
final ZoneController _zoneController = Get.put(ZoneController());
|
with AutomaticKeepAliveClientMixin {
|
||||||
|
late ZoneController _zoneController;
|
||||||
List videoList = [];
|
List videoList = [];
|
||||||
Future? _futureBuilderFuture;
|
Future? _futureBuilderFuture;
|
||||||
late ScrollController scrollController;
|
late ScrollController scrollController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_zoneController = Get.put(ZoneController(), tag: widget.rid.toString());
|
||||||
_futureBuilderFuture = _zoneController.queryRankFeed('init', widget.rid);
|
_futureBuilderFuture = _zoneController.queryRankFeed('init', widget.rid);
|
||||||
scrollController = _zoneController.scrollController;
|
scrollController = _zoneController.scrollController;
|
||||||
StreamController<bool> mainStream =
|
StreamController<bool> mainStream =
|
||||||
@ -68,6 +73,7 @@ class _ZonePageState extends State<ZonePage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
return await _zoneController.onRefresh();
|
return await _zoneController.onRefresh();
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import 'package:pilipala/utils/utils.dart';
|
|||||||
import 'package:pilipala/utils/video_utils.dart';
|
import 'package:pilipala/utils/video_utils.dart';
|
||||||
import 'package:screen_brightness/screen_brightness.dart';
|
import 'package:screen_brightness/screen_brightness.dart';
|
||||||
|
|
||||||
|
import '../../../models/video/subTitile/content.dart';
|
||||||
import '../../../http/danmaku.dart';
|
import '../../../http/danmaku.dart';
|
||||||
import '../../../utils/id_utils.dart';
|
import '../../../utils/id_utils.dart';
|
||||||
import 'widgets/header_control.dart';
|
import 'widgets/header_control.dart';
|
||||||
@ -93,7 +94,10 @@ class VideoDetailController extends GetxController
|
|||||||
late int cacheAudioQa;
|
late int cacheAudioQa;
|
||||||
|
|
||||||
PersistentBottomSheetController? replyReplyBottomSheetCtr;
|
PersistentBottomSheetController? replyReplyBottomSheetCtr;
|
||||||
|
RxList<SubTitileContentModel> subtitleContents =
|
||||||
|
<SubTitileContentModel>[].obs;
|
||||||
late bool enableRelatedVideo;
|
late bool enableRelatedVideo;
|
||||||
|
List subtitles = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -145,6 +149,7 @@ class VideoDetailController extends GetxController
|
|||||||
cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
|
cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
|
||||||
defaultValue: AudioQuality.hiRes.code);
|
defaultValue: AudioQuality.hiRes.code);
|
||||||
oid.value = IdUtils.bv2av(Get.parameters['bvid']!);
|
oid.value = IdUtils.bv2av(Get.parameters['bvid']!);
|
||||||
|
getSubtitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
showReplyReplyPanel() {
|
showReplyReplyPanel() {
|
||||||
@ -251,6 +256,8 @@ class VideoDetailController extends GetxController
|
|||||||
|
|
||||||
/// 开启自动全屏时,在player初始化完成后立即传入headerControl
|
/// 开启自动全屏时,在player初始化完成后立即传入headerControl
|
||||||
plPlayerController.headerControl = headerControl;
|
plPlayerController.headerControl = headerControl;
|
||||||
|
|
||||||
|
plPlayerController.subtitles.value = subtitles;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 视频链接
|
// 视频链接
|
||||||
@ -388,6 +395,45 @@ class VideoDetailController extends GetxController
|
|||||||
: print('replyReplyBottomSheetCtr is null');
|
: print('replyReplyBottomSheetCtr is null');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取字幕配置
|
||||||
|
Future getSubtitle() async {
|
||||||
|
var result = await VideoHttp.getSubtitle(bvid: bvid, cid: cid.value);
|
||||||
|
if (result['status']) {
|
||||||
|
if (result['data'].subtitles.isNotEmpty) {
|
||||||
|
subtitles = result['data'].subtitles;
|
||||||
|
if (subtitles.isNotEmpty) {
|
||||||
|
for (var i in subtitles) {
|
||||||
|
final Map<String, dynamic> res = await VideoHttp.getSubtitleContent(
|
||||||
|
i.subtitleUrl,
|
||||||
|
);
|
||||||
|
i.content = res['content'];
|
||||||
|
i.body = res['body'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result['data'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取字幕内容
|
||||||
|
// Future getSubtitleContent(String url) async {
|
||||||
|
// var res = await Request().get('https:$url');
|
||||||
|
// subtitleContents.value = res.data['body'].map<SubTitileContentModel>((e) {
|
||||||
|
// return SubTitileContentModel.fromJson(e);
|
||||||
|
// }).toList();
|
||||||
|
// setSubtitleContent();
|
||||||
|
// }
|
||||||
|
|
||||||
|
setSubtitleContent() {
|
||||||
|
plPlayerController.subtitleContent.value = '';
|
||||||
|
plPlayerController.subtitles.value = subtitles;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSubtitleContent() {
|
||||||
|
plPlayerController.subtitleContent.value = '';
|
||||||
|
plPlayerController.subtitles.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
/// 发送弹幕
|
/// 发送弹幕
|
||||||
void showShootDanmakuSheet() {
|
void showShootDanmakuSheet() {
|
||||||
final TextEditingController textController = TextEditingController();
|
final TextEditingController textController = TextEditingController();
|
||||||
|
|||||||
@ -148,34 +148,16 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
|||||||
floating: true,
|
floating: true,
|
||||||
delegate: _MySliverPersistentHeaderDelegate(
|
delegate: _MySliverPersistentHeaderDelegate(
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 45,
|
height: 40,
|
||||||
padding: const EdgeInsets.fromLTRB(12, 0, 6, 0),
|
padding: const EdgeInsets.fromLTRB(12, 0, 6, 0),
|
||||||
decoration: BoxDecoration(
|
color: Theme.of(context).colorScheme.surface,
|
||||||
color: Theme.of(context).colorScheme.background,
|
|
||||||
border: Border(
|
|
||||||
bottom: BorderSide(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.outline
|
|
||||||
.withOpacity(0.1)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Obx(
|
Obx(
|
||||||
() => AnimatedSwitcher(
|
() => Text(
|
||||||
duration: const Duration(milliseconds: 400),
|
'${_videoReplyController.sortTypeLabel.value}评论',
|
||||||
transitionBuilder:
|
style: const TextStyle(fontSize: 13),
|
||||||
(Widget child, Animation<double> animation) {
|
|
||||||
return ScaleTransition(
|
|
||||||
scale: animation, child: child);
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
'共${_videoReplyController.count.value}条回复',
|
|
||||||
key: ValueKey<int>(
|
|
||||||
_videoReplyController.count.value),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
@ -184,10 +166,12 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
|||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
_videoReplyController.queryBySort(),
|
_videoReplyController.queryBySort(),
|
||||||
icon: const Icon(Icons.sort, size: 16),
|
icon: const Icon(Icons.sort, size: 16),
|
||||||
label: Obx(() => Text(
|
label: Obx(
|
||||||
_videoReplyController.sortTypeLabel.value,
|
() => Text(
|
||||||
style: const TextStyle(fontSize: 13),
|
_videoReplyController.sortTypeLabel.value,
|
||||||
)),
|
style: const TextStyle(fontSize: 13),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -329,8 +313,8 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
|||||||
|
|
||||||
class _MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
|
class _MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
|
||||||
_MySliverPersistentHeaderDelegate({required this.child});
|
_MySliverPersistentHeaderDelegate({required this.child});
|
||||||
final double _minExtent = 45;
|
final double _minExtent = 40;
|
||||||
final double _maxExtent = 45;
|
final double _maxExtent = 40;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -498,7 +498,7 @@ InlineSpan buildContent(
|
|||||||
return str;
|
return str;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// content.message = content.message.replaceAll(RegExp(r"\{vote:.*?\}"), ' ');
|
content.message = content.message.replaceAll(RegExp(r"\{vote:.*?\}"), ' ');
|
||||||
content.message = content.message
|
content.message = content.message
|
||||||
.replaceAll('&', '&')
|
.replaceAll('&', '&')
|
||||||
.replaceAll('<', '<')
|
.replaceAll('<', '<')
|
||||||
|
|||||||
@ -212,6 +212,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
videoIntroController.isPaused = true;
|
videoIntroController.isPaused = true;
|
||||||
plPlayerController!.removeStatusLister(playerListener);
|
plPlayerController!.removeStatusLister(playerListener);
|
||||||
plPlayerController!.pause();
|
plPlayerController!.pause();
|
||||||
|
vdCtr.clearSubtitleContent();
|
||||||
}
|
}
|
||||||
setState(() => isShowing = false);
|
setState(() => isShowing = false);
|
||||||
super.didPushNext();
|
super.didPushNext();
|
||||||
@ -222,7 +223,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
void didPopNext() async {
|
void didPopNext() async {
|
||||||
if (plPlayerController != null &&
|
if (plPlayerController != null &&
|
||||||
plPlayerController!.videoPlayerController != null) {
|
plPlayerController!.videoPlayerController != null) {
|
||||||
setState(() => isShowing = true);
|
setState(() {
|
||||||
|
vdCtr.setSubtitleContent();
|
||||||
|
isShowing = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
vdCtr.isFirstTime = false;
|
vdCtr.isFirstTime = false;
|
||||||
final bool autoplay = autoPlayEnable;
|
final bool autoplay = autoPlayEnable;
|
||||||
|
|||||||
@ -17,7 +17,6 @@ import 'package:pilipala/plugin/pl_player/index.dart';
|
|||||||
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
|
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
|
||||||
import 'package:pilipala/utils/storage.dart';
|
import 'package:pilipala/utils/storage.dart';
|
||||||
import 'package:pilipala/services/shutdown_timer_service.dart';
|
import 'package:pilipala/services/shutdown_timer_service.dart';
|
||||||
import '../../../../http/danmaku.dart';
|
|
||||||
import '../../../../models/common/search_type.dart';
|
import '../../../../models/common/search_type.dart';
|
||||||
import '../../../../models/video_detail_res.dart';
|
import '../../../../models/video_detail_res.dart';
|
||||||
import '../introduction/index.dart';
|
import '../introduction/index.dart';
|
||||||
@ -53,7 +52,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
|||||||
final Box<dynamic> videoStorage = GStrorage.video;
|
final Box<dynamic> videoStorage = GStrorage.video;
|
||||||
late List<double> speedsList;
|
late List<double> speedsList;
|
||||||
double buttonSpace = 8;
|
double buttonSpace = 8;
|
||||||
RxBool isFullScreen = false.obs;
|
bool showTitle = false;
|
||||||
late String heroTag;
|
late String heroTag;
|
||||||
late VideoIntroController videoIntroController;
|
late VideoIntroController videoIntroController;
|
||||||
late VideoDetailData videoDetail;
|
late VideoDetailData videoDetail;
|
||||||
@ -70,8 +69,13 @@ class _HeaderControlState extends State<HeaderControl> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void fullScreenStatusListener() {
|
void fullScreenStatusListener() {
|
||||||
widget.videoDetailCtr!.plPlayerController.isFullScreen.listen((bool val) {
|
widget.videoDetailCtr!.plPlayerController.isFullScreen
|
||||||
isFullScreen.value = val;
|
.listen((bool isFullScreen) {
|
||||||
|
if (isFullScreen) {
|
||||||
|
showTitle = true;
|
||||||
|
} else {
|
||||||
|
showTitle = false;
|
||||||
|
}
|
||||||
|
|
||||||
/// TODO setState() called after dispose()
|
/// TODO setState() called after dispose()
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@ -214,87 +218,6 @@ class _HeaderControlState extends State<HeaderControl> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 发送弹幕
|
|
||||||
void showShootDanmakuSheet() {
|
|
||||||
final TextEditingController textController = TextEditingController();
|
|
||||||
bool isSending = false; // 追踪是否正在发送
|
|
||||||
showDialog(
|
|
||||||
context: Get.context!,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
// TODO: 支持更多类型和颜色的弹幕
|
|
||||||
return AlertDialog(
|
|
||||||
title: const Text('发送弹幕(测试)'),
|
|
||||||
content: StatefulBuilder(
|
|
||||||
builder: (BuildContext context, StateSetter setState) {
|
|
||||||
return TextField(
|
|
||||||
controller: textController,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
child: Text(
|
|
||||||
'取消',
|
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
StatefulBuilder(
|
|
||||||
builder: (BuildContext context, StateSetter setState) {
|
|
||||||
return TextButton(
|
|
||||||
onPressed: isSending
|
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
final String msg = textController.text;
|
|
||||||
if (msg.isEmpty) {
|
|
||||||
SmartDialog.showToast('弹幕内容不能为空');
|
|
||||||
return;
|
|
||||||
} else if (msg.length > 100) {
|
|
||||||
SmartDialog.showToast('弹幕内容不能超过100个字符');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
isSending = true; // 开始发送,更新状态
|
|
||||||
});
|
|
||||||
//修改按钮文字
|
|
||||||
final dynamic res = await DanmakaHttp.shootDanmaku(
|
|
||||||
oid: widget.videoDetailCtr!.cid.value,
|
|
||||||
msg: textController.text,
|
|
||||||
bvid: widget.videoDetailCtr!.bvid,
|
|
||||||
progress:
|
|
||||||
widget.controller!.position.value.inMilliseconds,
|
|
||||||
type: 1,
|
|
||||||
);
|
|
||||||
setState(() {
|
|
||||||
isSending = false; // 发送结束,更新状态
|
|
||||||
});
|
|
||||||
if (res['status']) {
|
|
||||||
SmartDialog.showToast('发送成功');
|
|
||||||
// 发送成功,自动预览该弹幕,避免重新请求
|
|
||||||
// TODO: 暂停状态下预览弹幕仍会移动与计时,可考虑添加到dmSegList或其他方式实现
|
|
||||||
widget.controller!.danmakuController!.addItems([
|
|
||||||
DanmakuItem(
|
|
||||||
msg,
|
|
||||||
color: Colors.white,
|
|
||||||
time: widget
|
|
||||||
.controller!.position.value.inMilliseconds,
|
|
||||||
type: DanmakuItemType.scroll,
|
|
||||||
isSend: true,
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
Get.back();
|
|
||||||
} else {
|
|
||||||
SmartDialog.showToast('发送失败,错误信息为${res['msg']}');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text(isSending ? '发送中...' : '发送'),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 定时关闭
|
/// 定时关闭
|
||||||
void scheduleExit() async {
|
void scheduleExit() async {
|
||||||
const List<int> scheduleTimeChoices = [
|
const List<int> scheduleTimeChoices = [
|
||||||
@ -421,6 +344,56 @@ class _HeaderControlState extends State<HeaderControl> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 选择字幕
|
||||||
|
void showSubtitleDialog() async {
|
||||||
|
int tempThemeValue = widget.controller!.subTitleCode.value;
|
||||||
|
int len = widget.videoDetailCtr!.subtitles.length;
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('选择字幕'),
|
||||||
|
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 18),
|
||||||
|
content: StatefulBuilder(builder: (context, StateSetter setState) {
|
||||||
|
return len == 0
|
||||||
|
? const SizedBox(
|
||||||
|
height: 60,
|
||||||
|
child: Center(
|
||||||
|
child: Text('没有字幕'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
RadioListTile(
|
||||||
|
value: -1,
|
||||||
|
title: const Text('关闭弹幕'),
|
||||||
|
groupValue: tempThemeValue,
|
||||||
|
onChanged: (value) {
|
||||||
|
tempThemeValue = value!;
|
||||||
|
widget.controller?.toggleSubtitle(value);
|
||||||
|
Get.back();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
...widget.videoDetailCtr!.subtitles
|
||||||
|
.map((e) => RadioListTile(
|
||||||
|
value: e.code,
|
||||||
|
title: Text(e.title),
|
||||||
|
groupValue: tempThemeValue,
|
||||||
|
onChanged: (value) {
|
||||||
|
tempThemeValue = value!;
|
||||||
|
widget.controller?.toggleSubtitle(value);
|
||||||
|
Get.back();
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// 选择倍速
|
/// 选择倍速
|
||||||
void showSetSpeedSheet() {
|
void showSetSpeedSheet() {
|
||||||
final double currentSpeed = widget.controller!.playbackSpeed;
|
final double currentSpeed = widget.controller!.playbackSpeed;
|
||||||
@ -1106,7 +1079,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
SizedBox(width: buttonSpace),
|
SizedBox(width: buttonSpace),
|
||||||
if (isFullScreen.value &&
|
if (showTitle &&
|
||||||
isLandscape &&
|
isLandscape &&
|
||||||
widget.videoType == SearchType.video) ...[
|
widget.videoType == SearchType.video) ...[
|
||||||
Column(
|
Column(
|
||||||
@ -1158,43 +1131,6 @@ class _HeaderControlState extends State<HeaderControl> {
|
|||||||
// ),
|
// ),
|
||||||
// fuc: () => _.screenshot(),
|
// fuc: () => _.screenshot(),
|
||||||
// ),
|
// ),
|
||||||
if (isFullScreen.value) ...[
|
|
||||||
SizedBox(
|
|
||||||
width: 56,
|
|
||||||
height: 34,
|
|
||||||
child: TextButton(
|
|
||||||
style: ButtonStyle(
|
|
||||||
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
|
||||||
),
|
|
||||||
onPressed: () => showShootDanmakuSheet(),
|
|
||||||
child: const Text(
|
|
||||||
'发弹幕',
|
|
||||||
style: textStyle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 34,
|
|
||||||
height: 34,
|
|
||||||
child: Obx(
|
|
||||||
() => IconButton(
|
|
||||||
style: ButtonStyle(
|
|
||||||
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
_.isOpenDanmu.value = !_.isOpenDanmu.value;
|
|
||||||
},
|
|
||||||
icon: Icon(
|
|
||||||
_.isOpenDanmu.value
|
|
||||||
? Icons.subtitles_outlined
|
|
||||||
: Icons.subtitles_off_outlined,
|
|
||||||
size: 19,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
SizedBox(width: buttonSpace),
|
SizedBox(width: buttonSpace),
|
||||||
if (Platform.isAndroid) ...<Widget>[
|
if (Platform.isAndroid) ...<Widget>[
|
||||||
SizedBox(
|
SizedBox(
|
||||||
@ -1229,6 +1165,31 @@ class _HeaderControlState extends State<HeaderControl> {
|
|||||||
),
|
),
|
||||||
SizedBox(width: buttonSpace),
|
SizedBox(width: buttonSpace),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
/// 字幕
|
||||||
|
// SizedBox(
|
||||||
|
// width: 34,
|
||||||
|
// height: 34,
|
||||||
|
// child: IconButton(
|
||||||
|
// style: ButtonStyle(
|
||||||
|
// padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||||
|
// ),
|
||||||
|
// onPressed: () => showSubtitleDialog(),
|
||||||
|
// icon: const Icon(
|
||||||
|
// Icons.closed_caption_off,
|
||||||
|
// size: 22,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
ComBtn(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.closed_caption_off,
|
||||||
|
size: 22,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
fuc: () => showSubtitleDialog(),
|
||||||
|
),
|
||||||
|
SizedBox(width: buttonSpace),
|
||||||
Obx(
|
Obx(
|
||||||
() => SizedBox(
|
() => SizedBox(
|
||||||
width: 45,
|
width: 45,
|
||||||
|
|||||||
@ -21,6 +21,8 @@ import 'package:pilipala/utils/storage.dart';
|
|||||||
import 'package:screen_brightness/screen_brightness.dart';
|
import 'package:screen_brightness/screen_brightness.dart';
|
||||||
import 'package:status_bar_control/status_bar_control.dart';
|
import 'package:status_bar_control/status_bar_control.dart';
|
||||||
import 'package:universal_platform/universal_platform.dart';
|
import 'package:universal_platform/universal_platform.dart';
|
||||||
|
import '../../models/video/subTitile/content.dart';
|
||||||
|
import '../../models/video/subTitile/result.dart';
|
||||||
// import 'package:wakelock_plus/wakelock_plus.dart';
|
// import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
|
|
||||||
Box videoStorage = GStrorage.video;
|
Box videoStorage = GStrorage.video;
|
||||||
@ -73,6 +75,8 @@ class PlPlayerController {
|
|||||||
final Rx<bool> _doubleSpeedStatus = false.obs;
|
final Rx<bool> _doubleSpeedStatus = false.obs;
|
||||||
final Rx<bool> _controlsLock = false.obs;
|
final Rx<bool> _controlsLock = false.obs;
|
||||||
final Rx<bool> _isFullScreen = false.obs;
|
final Rx<bool> _isFullScreen = false.obs;
|
||||||
|
final Rx<bool> _subTitleOpen = false.obs;
|
||||||
|
final Rx<int> _subTitleCode = (-1).obs;
|
||||||
// 默认投稿视频格式
|
// 默认投稿视频格式
|
||||||
static Rx<String> _videoType = 'archive'.obs;
|
static Rx<String> _videoType = 'archive'.obs;
|
||||||
|
|
||||||
@ -118,6 +122,7 @@ class PlPlayerController {
|
|||||||
PreferredSizeWidget? headerControl;
|
PreferredSizeWidget? headerControl;
|
||||||
PreferredSizeWidget? bottomControl;
|
PreferredSizeWidget? bottomControl;
|
||||||
Widget? danmuWidget;
|
Widget? danmuWidget;
|
||||||
|
late RxList subtitles;
|
||||||
|
|
||||||
/// 数据加载监听
|
/// 数据加载监听
|
||||||
Stream<DataStatus> get onDataStatusChanged => dataStatus.status.stream;
|
Stream<DataStatus> get onDataStatusChanged => dataStatus.status.stream;
|
||||||
@ -147,6 +152,11 @@ class PlPlayerController {
|
|||||||
Rx<bool> get mute => _mute;
|
Rx<bool> get mute => _mute;
|
||||||
Stream<bool> get onMuteChanged => _mute.stream;
|
Stream<bool> get onMuteChanged => _mute.stream;
|
||||||
|
|
||||||
|
/// 字幕开启状态
|
||||||
|
Rx<bool> get subTitleOpen => _subTitleOpen;
|
||||||
|
Rx<int> get subTitleCode => _subTitleCode;
|
||||||
|
// Stream<bool> get onSubTitleOpenChanged => _subTitleOpen.stream;
|
||||||
|
|
||||||
/// [videoPlayerController] instace of Player
|
/// [videoPlayerController] instace of Player
|
||||||
Player? get videoPlayerController => _videoPlayerController;
|
Player? get videoPlayerController => _videoPlayerController;
|
||||||
|
|
||||||
@ -231,6 +241,10 @@ class PlPlayerController {
|
|||||||
// 播放顺序相关
|
// 播放顺序相关
|
||||||
PlayRepeat playRepeat = PlayRepeat.pause;
|
PlayRepeat playRepeat = PlayRepeat.pause;
|
||||||
|
|
||||||
|
RxList<SubTitileContentModel> subtitleContents =
|
||||||
|
<SubTitileContentModel>[].obs;
|
||||||
|
RxString subtitleContent = ''.obs;
|
||||||
|
|
||||||
void updateSliderPositionSecond() {
|
void updateSliderPositionSecond() {
|
||||||
int newSecond = _sliderPosition.value.inSeconds;
|
int newSecond = _sliderPosition.value.inSeconds;
|
||||||
if (sliderPositionSeconds.value != newSecond) {
|
if (sliderPositionSeconds.value != newSecond) {
|
||||||
@ -350,6 +364,8 @@ class PlPlayerController {
|
|||||||
bool enableHeart = true,
|
bool enableHeart = true,
|
||||||
// 是否首次加载
|
// 是否首次加载
|
||||||
bool isFirstTime = true,
|
bool isFirstTime = true,
|
||||||
|
// 是否开启字幕
|
||||||
|
bool enableSubTitle = false,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
_autoPlay = autoplay;
|
_autoPlay = autoplay;
|
||||||
@ -364,7 +380,9 @@ class PlPlayerController {
|
|||||||
_cid = cid;
|
_cid = cid;
|
||||||
_enableHeart = enableHeart;
|
_enableHeart = enableHeart;
|
||||||
_isFirstTime = isFirstTime;
|
_isFirstTime = isFirstTime;
|
||||||
|
_subTitleOpen.value = enableSubTitle;
|
||||||
|
subtitles = [].obs;
|
||||||
|
subtitleContent.value = '';
|
||||||
if (_videoPlayerController != null &&
|
if (_videoPlayerController != null &&
|
||||||
_videoPlayerController!.state.playing) {
|
_videoPlayerController!.state.playing) {
|
||||||
await pause(notify: false);
|
await pause(notify: false);
|
||||||
@ -375,7 +393,13 @@ class PlPlayerController {
|
|||||||
}
|
}
|
||||||
// 配置Player 音轨、字幕等等
|
// 配置Player 音轨、字幕等等
|
||||||
_videoPlayerController = await _createVideoController(
|
_videoPlayerController = await _createVideoController(
|
||||||
dataSource, _looping, enableHA, width, height);
|
dataSource,
|
||||||
|
_looping,
|
||||||
|
enableHA,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
seekTo,
|
||||||
|
);
|
||||||
// 获取视频时长 00:00
|
// 获取视频时长 00:00
|
||||||
_duration.value = duration ?? _videoPlayerController!.state.duration;
|
_duration.value = duration ?? _videoPlayerController!.state.duration;
|
||||||
updateDurationSecond();
|
updateDurationSecond();
|
||||||
@ -386,7 +410,7 @@ class PlPlayerController {
|
|||||||
if (!_listenersInitialized) {
|
if (!_listenersInitialized) {
|
||||||
startListeners();
|
startListeners();
|
||||||
}
|
}
|
||||||
await _initializePlayer(seekTo: seekTo, duration: _duration.value);
|
await _initializePlayer(duration: _duration.value);
|
||||||
bool autoEnterFullcreen =
|
bool autoEnterFullcreen =
|
||||||
setting.get(SettingBoxKey.enableAutoEnter, defaultValue: false);
|
setting.get(SettingBoxKey.enableAutoEnter, defaultValue: false);
|
||||||
if (autoEnterFullcreen && _isFirstTime) {
|
if (autoEnterFullcreen && _isFirstTime) {
|
||||||
@ -406,6 +430,7 @@ class PlPlayerController {
|
|||||||
bool enableHA,
|
bool enableHA,
|
||||||
double? width,
|
double? width,
|
||||||
double? height,
|
double? height,
|
||||||
|
Duration? seekTo,
|
||||||
) async {
|
) async {
|
||||||
// 每次配置时先移除监听
|
// 每次配置时先移除监听
|
||||||
removeListeners();
|
removeListeners();
|
||||||
@ -488,7 +513,11 @@ class PlPlayerController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
player.open(
|
player.open(
|
||||||
Media(dataSource.videoSource!, httpHeaders: dataSource.httpHeaders),
|
Media(
|
||||||
|
dataSource.videoSource!,
|
||||||
|
httpHeaders: dataSource.httpHeaders,
|
||||||
|
start: seekTo ?? Duration.zero,
|
||||||
|
),
|
||||||
play: false,
|
play: false,
|
||||||
);
|
);
|
||||||
// 音轨
|
// 音轨
|
||||||
@ -501,7 +530,6 @@ class PlPlayerController {
|
|||||||
|
|
||||||
// 开始播放
|
// 开始播放
|
||||||
Future _initializePlayer({
|
Future _initializePlayer({
|
||||||
Duration seekTo = Duration.zero,
|
|
||||||
Duration? duration,
|
Duration? duration,
|
||||||
}) async {
|
}) async {
|
||||||
// 设置倍速
|
// 设置倍速
|
||||||
@ -519,11 +547,6 @@ class PlPlayerController {
|
|||||||
// await setLooping(_looping);
|
// await setLooping(_looping);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// 跳转播放
|
|
||||||
if (seekTo != Duration.zero) {
|
|
||||||
await this.seekTo(seekTo);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自动播放
|
// 自动播放
|
||||||
if (_autoPlay) {
|
if (_autoPlay) {
|
||||||
await play(duration: duration);
|
await play(duration: duration);
|
||||||
@ -575,6 +598,8 @@ class PlPlayerController {
|
|||||||
_sliderPosition.value = event;
|
_sliderPosition.value = event;
|
||||||
updateSliderPositionSecond();
|
updateSliderPositionSecond();
|
||||||
}
|
}
|
||||||
|
querySubtitleContent(
|
||||||
|
videoPlayerController!.state.position.inSeconds.toDouble());
|
||||||
|
|
||||||
/// 触发回调事件
|
/// 触发回调事件
|
||||||
for (var element in _positionListeners) {
|
for (var element in _positionListeners) {
|
||||||
@ -609,6 +634,10 @@ class PlPlayerController {
|
|||||||
const Duration(seconds: 1),
|
const Duration(seconds: 1),
|
||||||
() => videoPlayerServiceHandler.onPositionChange(event));
|
() => videoPlayerServiceHandler.onPositionChange(event));
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// onSubTitleOpenChanged.listen((bool event) {
|
||||||
|
// toggleSubtitle(event ? subTitleCode.value : -1);
|
||||||
|
// })
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -637,21 +666,14 @@ class PlPlayerController {
|
|||||||
await _videoPlayerController?.stream.buffer.first;
|
await _videoPlayerController?.stream.buffer.first;
|
||||||
}
|
}
|
||||||
await _videoPlayerController?.seek(position);
|
await _videoPlayerController?.seek(position);
|
||||||
// if (playerStatus.stopped) {
|
|
||||||
// play();
|
|
||||||
// }
|
|
||||||
} else {
|
} else {
|
||||||
print('seek duration else');
|
print('seek duration else');
|
||||||
_timerForSeek?.cancel();
|
_timerForSeek?.cancel();
|
||||||
_timerForSeek =
|
_timerForSeek =
|
||||||
Timer.periodic(const Duration(milliseconds: 200), (Timer t) async {
|
Timer.periodic(const Duration(milliseconds: 200), (Timer t) async {
|
||||||
//_timerForSeek = null;
|
|
||||||
if (duration.value.inSeconds != 0) {
|
if (duration.value.inSeconds != 0) {
|
||||||
await _videoPlayerController!.stream.buffer.first;
|
await _videoPlayerController!.stream.buffer.first;
|
||||||
await _videoPlayerController?.seek(position);
|
await _videoPlayerController?.seek(position);
|
||||||
// if (playerStatus.status.value == PlayerStatus.paused) {
|
|
||||||
// play();
|
|
||||||
// }
|
|
||||||
t.cancel();
|
t.cancel();
|
||||||
_timerForSeek = null;
|
_timerForSeek = null;
|
||||||
}
|
}
|
||||||
@ -1047,12 +1069,61 @@ class PlPlayerController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 字幕
|
||||||
|
void toggleSubtitle(int code) {
|
||||||
|
_subTitleOpen.value = code != -1;
|
||||||
|
_subTitleCode.value = code;
|
||||||
|
// if (code == -1) {
|
||||||
|
// // 关闭字幕
|
||||||
|
// _subTitleOpen.value = false;
|
||||||
|
// _subTitleCode.value = code;
|
||||||
|
// _videoPlayerController?.setSubtitleTrack(SubtitleTrack.no());
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// final SubTitlteItemModel? subtitle = subtitles?.firstWhereOrNull(
|
||||||
|
// (element) => element.code == code,
|
||||||
|
// );
|
||||||
|
// _subTitleOpen.value = true;
|
||||||
|
// _subTitleCode.value = code;
|
||||||
|
// _videoPlayerController?.setSubtitleTrack(
|
||||||
|
// SubtitleTrack.data(
|
||||||
|
// subtitle!.content!,
|
||||||
|
// title: subtitle.title,
|
||||||
|
// language: subtitle.lan,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
|
void querySubtitleContent(double progress) {
|
||||||
|
if (subTitleCode.value == -1) {
|
||||||
|
subtitleContent.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (subtitles.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final SubTitlteItemModel? subtitle = subtitles.firstWhereOrNull(
|
||||||
|
(element) => element.code == subTitleCode.value,
|
||||||
|
);
|
||||||
|
if (subtitle != null && subtitle.body!.isNotEmpty) {
|
||||||
|
for (var content in subtitle.body!) {
|
||||||
|
if (progress >= content['from']! && progress <= content['to']!) {
|
||||||
|
subtitleContent.value = content['content']!;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setPlayRepeat(PlayRepeat type) {
|
setPlayRepeat(PlayRepeat type) {
|
||||||
playRepeat = type;
|
playRepeat = type;
|
||||||
videoStorage.put(VideoBoxKey.playRepeat, type.value);
|
videoStorage.put(VideoBoxKey.playRepeat, type.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> dispose({String type = 'single'}) async {
|
Future<void> dispose({String type = 'single'}) async {
|
||||||
|
print('dispose');
|
||||||
|
print('dispose: ${playerCount.value}');
|
||||||
|
|
||||||
// 每次减1,最后销毁
|
// 每次减1,最后销毁
|
||||||
if (type == 'single' && playerCount.value > 1) {
|
if (type == 'single' && playerCount.value > 1) {
|
||||||
_playerCount.value -= 1;
|
_playerCount.value -= 1;
|
||||||
@ -1062,6 +1133,7 @@ class PlPlayerController {
|
|||||||
}
|
}
|
||||||
_playerCount.value = 0;
|
_playerCount.value = 0;
|
||||||
try {
|
try {
|
||||||
|
print('dispose dispose ---------');
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_timerForVolume?.cancel();
|
_timerForVolume?.cancel();
|
||||||
_timerForGettingVolume?.cancel();
|
_timerForGettingVolume?.cancel();
|
||||||
|
|||||||
@ -580,6 +580,45 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
if (widget.danmuWidget != null)
|
if (widget.danmuWidget != null)
|
||||||
Positioned.fill(top: 4, child: widget.danmuWidget!),
|
Positioned.fill(top: 4, child: widget.danmuWidget!),
|
||||||
|
|
||||||
|
/// 开启且有字幕时展示
|
||||||
|
Stack(
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 30,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Obx(
|
||||||
|
() => Visibility(
|
||||||
|
visible: widget.controller.subTitleCode.value != -1,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
color: widget.controller.subtitleContent.value != ''
|
||||||
|
? Colors.black.withOpacity(0.6)
|
||||||
|
: Colors.transparent,
|
||||||
|
),
|
||||||
|
padding: widget.controller.subTitleCode.value != -1
|
||||||
|
? const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10,
|
||||||
|
vertical: 4,
|
||||||
|
)
|
||||||
|
: EdgeInsets.zero,
|
||||||
|
child: Text(
|
||||||
|
widget.controller.subtitleContent.value,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
/// 手势
|
/// 手势
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
left: 16,
|
left: 16,
|
||||||
|
|||||||
@ -19,15 +19,7 @@ class Em {
|
|||||||
return regCate(matchStr);
|
return regCate(matchStr);
|
||||||
}, onNonMatch: (String str) {
|
}, onNonMatch: (String str) {
|
||||||
if (str != '') {
|
if (str != '') {
|
||||||
str = str
|
str = decodeHtmlEntities(str);
|
||||||
.replaceAll('<', '<')
|
|
||||||
.replaceAll('>', '>')
|
|
||||||
.replaceAll('"', '"')
|
|
||||||
.replaceAll(''', "'")
|
|
||||||
.replaceAll('"', '"')
|
|
||||||
.replaceAll(''', "'")
|
|
||||||
.replaceAll(' ', " ")
|
|
||||||
.replaceAll('&', "&");
|
|
||||||
Map map = {'type': 'text', 'text': str};
|
Map map = {'type': 'text', 'text': str};
|
||||||
res.add(map);
|
res.add(map);
|
||||||
}
|
}
|
||||||
@ -35,4 +27,17 @@ class Em {
|
|||||||
});
|
});
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String decodeHtmlEntities(String title) {
|
||||||
|
return title
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll(''', "'")
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll(''', "'")
|
||||||
|
.replaceAll(' ', " ")
|
||||||
|
.replaceAll('&', "&")
|
||||||
|
.replaceAll(''', "'");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
lib/utils/subtitle.dart
Normal file
32
lib/utils/subtitle.dart
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
class SubTitleUtils {
|
||||||
|
// 格式整理
|
||||||
|
static String convertToWebVTT(List jsonData) {
|
||||||
|
String webVTTContent = 'WEBVTT FILE\n\n';
|
||||||
|
|
||||||
|
for (int i = 0; i < jsonData.length; i++) {
|
||||||
|
final item = jsonData[i];
|
||||||
|
double from = item['from'] as double;
|
||||||
|
double to = item['to'] as double;
|
||||||
|
int sid = (item['sid'] ?? 0) as int;
|
||||||
|
String content = item['content'] as String;
|
||||||
|
|
||||||
|
webVTTContent += '$sid\n';
|
||||||
|
webVTTContent += '${formatTime(from)} --> ${formatTime(to)}\n';
|
||||||
|
webVTTContent += '$content\n\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
return webVTTContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String formatTime(num seconds) {
|
||||||
|
final String h = (seconds / 3600).floor().toString().padLeft(2, '0');
|
||||||
|
final String m = (seconds % 3600 / 60).floor().toString().padLeft(2, '0');
|
||||||
|
final String s = (seconds % 60).floor().toString().padLeft(2, '0');
|
||||||
|
final String ms =
|
||||||
|
(seconds * 1000 % 1000).floor().toString().padLeft(3, '0');
|
||||||
|
if (h == '00') {
|
||||||
|
return "$m:$s.$ms";
|
||||||
|
}
|
||||||
|
return "$h:$m:$s.$ms";
|
||||||
|
}
|
||||||
|
}
|
||||||
27
pubspec.lock
27
pubspec.lock
@ -865,10 +865,11 @@ packages:
|
|||||||
media_kit:
|
media_kit:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: media_kit
|
path: media_kit
|
||||||
sha256: "3289062540e3b8b9746e5c50d95bd78a9289826b7227e253dff806d002b9e67a"
|
ref: HEAD
|
||||||
url: "https://pub.flutter-io.cn"
|
resolved-ref: "77a130b1d7ce733b47d2133b57563716090450d0"
|
||||||
source: hosted
|
url: "https://github.com/media-kit/media-kit.git"
|
||||||
|
source: git
|
||||||
version: "1.1.10+1"
|
version: "1.1.10+1"
|
||||||
media_kit_libs_android_video:
|
media_kit_libs_android_video:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
@ -905,10 +906,11 @@ packages:
|
|||||||
media_kit_libs_video:
|
media_kit_libs_video:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: media_kit_libs_video
|
path: "libs/universal/media_kit_libs_video"
|
||||||
sha256: "3688e0c31482074578652bf038ce6301a5d21e1eda6b54fc3117ffeb4bdba067"
|
ref: HEAD
|
||||||
url: "https://pub.flutter-io.cn"
|
resolved-ref: "77a130b1d7ce733b47d2133b57563716090450d0"
|
||||||
source: hosted
|
url: "https://github.com/media-kit/media-kit.git"
|
||||||
|
source: git
|
||||||
version: "1.0.4"
|
version: "1.0.4"
|
||||||
media_kit_libs_windows_video:
|
media_kit_libs_windows_video:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
@ -929,10 +931,11 @@ packages:
|
|||||||
media_kit_video:
|
media_kit_video:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: media_kit_video
|
path: media_kit_video
|
||||||
sha256: c048d11a19e379aebbe810647636e3fc6d18374637e2ae12def4ff8a4b99a882
|
ref: HEAD
|
||||||
url: "https://pub.flutter-io.cn"
|
resolved-ref: "77a130b1d7ce733b47d2133b57563716090450d0"
|
||||||
source: hosted
|
url: "https://github.com/media-kit/media-kit.git"
|
||||||
|
source: git
|
||||||
version: "1.2.4"
|
version: "1.2.4"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
|
|||||||
14
pubspec.yaml
14
pubspec.yaml
@ -163,6 +163,20 @@ dev_dependencies:
|
|||||||
hive_generator: ^2.0.0
|
hive_generator: ^2.0.0
|
||||||
build_runner: ^2.4.8
|
build_runner: ^2.4.8
|
||||||
|
|
||||||
|
dependency_overrides:
|
||||||
|
media_kit:
|
||||||
|
git:
|
||||||
|
url: https://github.com/media-kit/media-kit.git
|
||||||
|
path: media_kit
|
||||||
|
media_kit_video:
|
||||||
|
git:
|
||||||
|
url: https://github.com/media-kit/media-kit.git
|
||||||
|
path: media_kit_video
|
||||||
|
media_kit_libs_video:
|
||||||
|
git:
|
||||||
|
url: https://github.com/media-kit/media-kit.git
|
||||||
|
path: libs/universal/media_kit_libs_video
|
||||||
|
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
android: true
|
android: true
|
||||||
ios: true
|
ios: true
|
||||||
|
|||||||
Reference in New Issue
Block a user