Compare commits
5 Commits
feature-da
...
feature-me
| Author | SHA1 | Date | |
|---|---|---|---|
| b689db16b5 | |||
| 76784ee664 | |||
| 2dbef3fee2 | |||
| 3a66c8c03d | |||
| 6f62837495 |
@ -506,6 +506,4 @@ class Api {
|
|||||||
/// 排行榜
|
/// 排行榜
|
||||||
static const String getRankApi = "/x/web-interface/ranking/v2";
|
static const String getRankApi = "/x/web-interface/ranking/v2";
|
||||||
|
|
||||||
/// 取消订阅
|
|
||||||
static const String cancelSub = '/x/v3/fav/season/unfav';
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -349,21 +349,4 @@ class UserHttp {
|
|||||||
return {'status': false, 'msg': res.data['message']};
|
return {'status': false, 'msg': res.data['message']};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取消订阅
|
|
||||||
static Future cancelSub({required int seasonId}) async {
|
|
||||||
var res = await Request().post(
|
|
||||||
Api.cancelSub,
|
|
||||||
queryParameters: {
|
|
||||||
'platform': 'web',
|
|
||||||
'season_id': seasonId,
|
|
||||||
'csrf': await Request.getCsrf(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (res.data['code'] == 0) {
|
|
||||||
return {'status': true};
|
|
||||||
} else {
|
|
||||||
return {'status': false, 'msg': res.data['message']};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,7 +34,6 @@ void main() async {
|
|||||||
.then((_) async {
|
.then((_) async {
|
||||||
await GStrorage.init();
|
await GStrorage.init();
|
||||||
await setupServiceLocator();
|
await setupServiceLocator();
|
||||||
clearLogs();
|
|
||||||
Request();
|
Request();
|
||||||
await Request.setCookie();
|
await Request.setCookie();
|
||||||
RecommendFilter();
|
RecommendFilter();
|
||||||
|
|||||||
@ -47,23 +47,18 @@ class Vip {
|
|||||||
this.status,
|
this.status,
|
||||||
this.dueDate,
|
this.dueDate,
|
||||||
this.label,
|
this.label,
|
||||||
this.nicknameColor,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
int? type;
|
int? type;
|
||||||
int? status;
|
int? status;
|
||||||
int? dueDate;
|
int? dueDate;
|
||||||
Map? label;
|
Map? label;
|
||||||
int? nicknameColor;
|
|
||||||
|
|
||||||
Vip.fromJson(Map<String, dynamic> json) {
|
Vip.fromJson(Map<String, dynamic> json) {
|
||||||
type = json['type'];
|
type = json['type'];
|
||||||
status = json['status'];
|
status = json['status'];
|
||||||
dueDate = json['due_date'];
|
dueDate = json['due_date'];
|
||||||
label = json['label'];
|
label = json['label'];
|
||||||
nicknameColor = json['nickname_color'] == ''
|
|
||||||
? null
|
|
||||||
: int.parse("0xFF${json['nickname_color'].replaceAll('#', '')}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -55,7 +55,13 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
|||||||
..addStatusLister(playerListener)
|
..addStatusLister(playerListener)
|
||||||
..addPositionListener(videoPositionListen);
|
..addPositionListener(videoPositionListen);
|
||||||
}
|
}
|
||||||
danmakuStatusListen();
|
playerController.isOpenDanmu.listen((p0) {
|
||||||
|
if (p0 && !_plDanmakuController.initiated) {
|
||||||
|
_plDanmakuController.initiate(
|
||||||
|
playerController.duration.value.inMilliseconds,
|
||||||
|
playerController.position.value.inMilliseconds);
|
||||||
|
}
|
||||||
|
});
|
||||||
blockTypes = playerController.blockTypes;
|
blockTypes = playerController.blockTypes;
|
||||||
showArea = playerController.showArea;
|
showArea = playerController.showArea;
|
||||||
opacityVal = playerController.opacityVal;
|
opacityVal = playerController.opacityVal;
|
||||||
@ -105,20 +111,6 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 弹幕开关状态监听
|
|
||||||
void danmakuStatusListen() {
|
|
||||||
playerController.isOpenDanmu.listen((p0) {
|
|
||||||
if (p0 && !_plDanmakuController.initiated) {
|
|
||||||
_plDanmakuController.initiate(
|
|
||||||
playerController.duration.value.inMilliseconds,
|
|
||||||
playerController.position.value.inMilliseconds);
|
|
||||||
} else {
|
|
||||||
playerController.danmakuController!.pause();
|
|
||||||
playerController.danmakuController!.clear();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
playerController.removePositionListener(videoPositionListen);
|
playerController.removePositionListener(videoPositionListen);
|
||||||
@ -130,7 +122,10 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
|||||||
return LayoutBuilder(builder: (context, box) {
|
return LayoutBuilder(builder: (context, box) {
|
||||||
// double initDuration = box.maxWidth / 12;
|
// double initDuration = box.maxWidth / 12;
|
||||||
return Obx(
|
return Obx(
|
||||||
() => DanmakuView(
|
() => AnimatedOpacity(
|
||||||
|
opacity: playerController.isOpenDanmu.value ? 1 : 0,
|
||||||
|
duration: const Duration(milliseconds: 100),
|
||||||
|
child: DanmakuView(
|
||||||
createdController: (DanmakuController e) async {
|
createdController: (DanmakuController e) async {
|
||||||
playerController.danmakuController = _controller = e;
|
playerController.danmakuController = _controller = e;
|
||||||
},
|
},
|
||||||
@ -141,13 +136,15 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
|||||||
hideTop: blockTypes.contains(5),
|
hideTop: blockTypes.contains(5),
|
||||||
hideScroll: blockTypes.contains(2),
|
hideScroll: blockTypes.contains(2),
|
||||||
hideBottom: blockTypes.contains(4),
|
hideBottom: blockTypes.contains(4),
|
||||||
duration: danmakuDurationVal / playerController.playbackSpeed,
|
duration:
|
||||||
|
danmakuDurationVal / playerController.playbackSpeed,
|
||||||
strokeWidth: strokeWidth,
|
strokeWidth: strokeWidth,
|
||||||
// initDuration /
|
// initDuration /
|
||||||
// (danmakuSpeedVal * widget.playerController.playbackSpeed),
|
// (danmakuSpeedVal * widget.playerController.playbackSpeed),
|
||||||
),
|
),
|
||||||
statusChanged: (isPlaying) {},
|
statusChanged: (isPlaying) {},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -258,27 +258,24 @@ class HistoryItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
videoItem.progress != 0
|
Positioned(
|
||||||
? Positioned(
|
|
||||||
left: 3,
|
left: 3,
|
||||||
right: 3,
|
right: 3,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
bottomLeft: Radius.circular(
|
bottomLeft:
|
||||||
StyleString.imgRadius.x),
|
Radius.circular(StyleString.imgRadius.x),
|
||||||
bottomRight: Radius.circular(
|
bottomRight:
|
||||||
StyleString.imgRadius.x),
|
Radius.circular(StyleString.imgRadius.x),
|
||||||
),
|
),
|
||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: videoItem.progress == -1
|
value: videoItem.progress == -1
|
||||||
? 100
|
? 100
|
||||||
: videoItem.progress /
|
: videoItem.progress / videoItem.duration,
|
||||||
videoItem.duration,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const SizedBox()
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
VideoContent(videoItem: videoItem, ctr: ctr)
|
VideoContent(videoItem: videoItem, ctr: ctr)
|
||||||
|
|||||||
@ -281,8 +281,8 @@ class _MemberPageState extends State<MemberPage>
|
|||||||
future: _futureBuilderFuture,
|
future: _futureBuilderFuture,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.done) {
|
if (snapshot.connectionState == ConnectionState.done) {
|
||||||
Map? data = snapshot.data;
|
Map data = snapshot.data!;
|
||||||
if (data != null && data['status']) {
|
if (data['status']) {
|
||||||
return Obx(
|
return Obx(
|
||||||
() => Stack(
|
() => Stack(
|
||||||
alignment: AlignmentDirectional.center,
|
alignment: AlignmentDirectional.center,
|
||||||
@ -302,14 +302,7 @@ class _MemberPageState extends State<MemberPage>
|
|||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.titleMedium!
|
.titleMedium!
|
||||||
.copyWith(
|
.copyWith(fontWeight: FontWeight.bold),
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: _memberController.memberInfo.value
|
|
||||||
.vip!.nicknameColor !=
|
|
||||||
null
|
|
||||||
? Color(_memberController.memberInfo
|
|
||||||
.value.vip!.nicknameColor!)
|
|
||||||
: null),
|
|
||||||
)),
|
)),
|
||||||
const SizedBox(width: 2),
|
const SizedBox(width: 2),
|
||||||
if (_memberController.memberInfo.value.sex == '女')
|
if (_memberController.memberInfo.value.sex == '女')
|
||||||
|
|||||||
@ -18,32 +18,45 @@ class MemberSeasonsPanel extends StatelessWidget {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
MemberSeasonsList item = data!.seasonsList![index];
|
MemberSeasonsList item = data!.seasonsList![index];
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
padding: const EdgeInsets.only(bottom: 12, right: 4),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
Padding(
|
||||||
onTap: () => Get.toNamed(
|
padding: const EdgeInsets.only(bottom: 12, left: 4),
|
||||||
'/memberSeasons?mid=${item.meta!.mid}&seasonId=${item.meta!.seasonId}'),
|
child: Row(
|
||||||
title: Text(
|
children: [
|
||||||
|
Text(
|
||||||
item.meta!.name!,
|
item.meta!.name!,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: Theme.of(context).textTheme.titleSmall!,
|
style: Theme.of(context).textTheme.titleSmall!,
|
||||||
),
|
),
|
||||||
dense: true,
|
const SizedBox(width: 10),
|
||||||
leading: PBadge(
|
PBadge(
|
||||||
stack: 'relative',
|
stack: 'relative',
|
||||||
size: 'small',
|
size: 'small',
|
||||||
text: item.meta!.total.toString(),
|
text: item.meta!.total.toString(),
|
||||||
),
|
),
|
||||||
trailing: const Icon(
|
const Spacer(),
|
||||||
|
SizedBox(
|
||||||
|
width: 35,
|
||||||
|
height: 35,
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () => Get.toNamed(
|
||||||
|
'/memberSeasons?mid=${item.meta!.mid}&seasonId=${item.meta!.seasonId}'),
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||||
|
),
|
||||||
|
icon: const Icon(
|
||||||
Icons.arrow_forward,
|
Icons.arrow_forward,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
LayoutBuilder(
|
LayoutBuilder(
|
||||||
builder: (context, boxConstraints) {
|
builder: (context, boxConstraints) {
|
||||||
return GridView.builder(
|
return GridView.builder(
|
||||||
|
|||||||
@ -46,40 +46,4 @@ class SubController extends GetxController {
|
|||||||
Future onLoad() async {
|
Future onLoad() async {
|
||||||
querySubFolder(type: 'onload');
|
querySubFolder(type: 'onload');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取消订阅
|
|
||||||
Future<void> cancelSub(SubFolderItemData subFolderItem) async {
|
|
||||||
showDialog(
|
|
||||||
context: Get.context!,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: const Text('提示'),
|
|
||||||
content: const Text('确定取消订阅吗?'),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Get.back();
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
'取消',
|
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
var res = await UserHttp.cancelSub(seasonId: subFolderItem.id!);
|
|
||||||
if (res['status']) {
|
|
||||||
subFolderData.value.list!.remove(subFolderItem);
|
|
||||||
subFolderData.update((val) {});
|
|
||||||
SmartDialog.showToast('取消订阅成功');
|
|
||||||
} else {
|
|
||||||
SmartDialog.showToast(res['msg']);
|
|
||||||
}
|
|
||||||
Get.back();
|
|
||||||
},
|
|
||||||
child: const Text('确定'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,8 +58,7 @@ class _SubPageState extends State<SubPage> {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return SubItem(
|
return SubItem(
|
||||||
subFolderItem:
|
subFolderItem:
|
||||||
_subController.subFolderData.value.list![index],
|
_subController.subFolderData.value.list![index]);
|
||||||
cancelSub: _subController.cancelSub);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -8,12 +8,7 @@ import '../../../models/user/sub_folder.dart';
|
|||||||
|
|
||||||
class SubItem extends StatelessWidget {
|
class SubItem extends StatelessWidget {
|
||||||
final SubFolderItemData subFolderItem;
|
final SubFolderItemData subFolderItem;
|
||||||
final Function(SubFolderItemData) cancelSub;
|
const SubItem({super.key, required this.subFolderItem});
|
||||||
const SubItem({
|
|
||||||
super.key,
|
|
||||||
required this.subFolderItem,
|
|
||||||
required this.cancelSub,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -56,10 +51,7 @@ class SubItem extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
VideoContent(
|
VideoContent(subFolderItem: subFolderItem)
|
||||||
subFolderItem: subFolderItem,
|
|
||||||
cancelSub: cancelSub,
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -72,8 +64,7 @@ class SubItem extends StatelessWidget {
|
|||||||
|
|
||||||
class VideoContent extends StatelessWidget {
|
class VideoContent extends StatelessWidget {
|
||||||
final SubFolderItemData subFolderItem;
|
final SubFolderItemData subFolderItem;
|
||||||
final Function(SubFolderItemData)? cancelSub;
|
const VideoContent({super.key, required this.subFolderItem});
|
||||||
const VideoContent({super.key, required this.subFolderItem, this.cancelSub});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -109,24 +100,6 @@ class VideoContent extends StatelessWidget {
|
|||||||
color: Theme.of(context).colorScheme.outline,
|
color: Theme.of(context).colorScheme.outline,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
height: 35,
|
|
||||||
width: 35,
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: () => cancelSub?.call(subFolderItem),
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
foregroundColor: Theme.of(context).colorScheme.outline,
|
|
||||||
padding: const EdgeInsets.fromLTRB(0, 0, 0, 0),
|
|
||||||
),
|
|
||||||
icon: const Icon(Icons.delete_outline, size: 18),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -525,18 +525,14 @@ InlineSpan buildContent(
|
|||||||
if (jumpUrlKeysList.isNotEmpty) {
|
if (jumpUrlKeysList.isNotEmpty) {
|
||||||
patternStr += '|${jumpUrlKeysList.join('|')}';
|
patternStr += '|${jumpUrlKeysList.join('|')}';
|
||||||
}
|
}
|
||||||
RegExp bv23Regex = RegExp(r'https://b23\.tv/[a-zA-Z0-9]{7}');
|
|
||||||
final RegExp pattern = RegExp(patternStr);
|
final RegExp pattern = RegExp(patternStr);
|
||||||
List<String> matchedStrs = [];
|
List<String> matchedStrs = [];
|
||||||
void addPlainTextSpan(str) {
|
void addPlainTextSpan(str) {
|
||||||
spanChilds.add(
|
spanChilds.add(TextSpan(
|
||||||
TextSpan(
|
|
||||||
text: str,
|
text: str,
|
||||||
recognizer: TapGestureRecognizer()
|
recognizer: TapGestureRecognizer()
|
||||||
..onTap = () =>
|
..onTap = () =>
|
||||||
replyReply?.call(replyItem.root == 0 ? replyItem : fReplyItem),
|
replyReply?.call(replyItem.root == 0 ? replyItem : fReplyItem)));
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分割文本并处理每个部分
|
// 分割文本并处理每个部分
|
||||||
@ -738,36 +734,8 @@ InlineSpan buildContent(
|
|||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
onNonMatch: (String nonMatchStr) {
|
onNonMatch: (String nonMatchStr) {
|
||||||
return nonMatchStr.splitMapJoin(
|
addPlainTextSpan(nonMatchStr);
|
||||||
bv23Regex,
|
return nonMatchStr;
|
||||||
onMatch: (Match match) {
|
|
||||||
String matchStr = match[0]!;
|
|
||||||
spanChilds.add(
|
|
||||||
TextSpan(
|
|
||||||
text: ' $matchStr ',
|
|
||||||
style: isVideoPage
|
|
||||||
? TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
recognizer: TapGestureRecognizer()
|
|
||||||
..onTap = () => Get.toNamed(
|
|
||||||
'/webview',
|
|
||||||
parameters: {
|
|
||||||
'url': matchStr,
|
|
||||||
'type': 'url',
|
|
||||||
'pageTitle': matchStr
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return '';
|
|
||||||
},
|
|
||||||
onNonMatch: (String nonMatchOtherStr) {
|
|
||||||
addPlainTextSpan(nonMatchOtherStr);
|
|
||||||
return nonMatchOtherStr;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -142,10 +142,9 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
double _keyboardHeight = EdgeInsets.fromViewPadding(
|
double keyboardHeight = EdgeInsets.fromViewPadding(
|
||||||
View.of(context).viewInsets, View.of(context).devicePixelRatio)
|
View.of(context).viewInsets, View.of(context).devicePixelRatio)
|
||||||
.bottom;
|
.bottom;
|
||||||
print('_keyboardHeight: $_keyboardHeight');
|
|
||||||
return Container(
|
return Container(
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -236,11 +235,7 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
|
|||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: toolbarType == 'input'
|
height: toolbarType == 'input' ? keyboardHeight : emoteHeight,
|
||||||
? (_keyboardHeight > keyboardHeight
|
|
||||||
? _keyboardHeight
|
|
||||||
: keyboardHeight)
|
|
||||||
: emoteHeight,
|
|
||||||
child: EmotePanel(
|
child: EmotePanel(
|
||||||
onChoose: (package, emote) => onChooseEmote(package, emote),
|
onChoose: (package, emote) => onChooseEmote(package, emote),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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 = [
|
||||||
@ -1156,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(
|
||||||
@ -1208,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(
|
||||||
|
|||||||
@ -393,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();
|
||||||
@ -404,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) {
|
||||||
@ -424,6 +430,7 @@ class PlPlayerController {
|
|||||||
bool enableHA,
|
bool enableHA,
|
||||||
double? width,
|
double? width,
|
||||||
double? height,
|
double? height,
|
||||||
|
Duration? seekTo,
|
||||||
) async {
|
) async {
|
||||||
// 每次配置时先移除监听
|
// 每次配置时先移除监听
|
||||||
removeListeners();
|
removeListeners();
|
||||||
@ -506,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,
|
||||||
);
|
);
|
||||||
// 音轨
|
// 音轨
|
||||||
@ -519,7 +530,6 @@ class PlPlayerController {
|
|||||||
|
|
||||||
// 开始播放
|
// 开始播放
|
||||||
Future _initializePlayer({
|
Future _initializePlayer({
|
||||||
Duration seekTo = Duration.zero,
|
|
||||||
Duration? duration,
|
Duration? duration,
|
||||||
}) async {
|
}) async {
|
||||||
// 设置倍速
|
// 设置倍速
|
||||||
@ -537,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);
|
||||||
@ -661,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,7 +51,7 @@ class Utils {
|
|||||||
}
|
}
|
||||||
if (time < 3600) {
|
if (time < 3600) {
|
||||||
if (time == 0) {
|
if (time == 0) {
|
||||||
return '00:00';
|
return time;
|
||||||
}
|
}
|
||||||
final int minute = time ~/ 60;
|
final int minute = time ~/ 60;
|
||||||
final double res = time / 60;
|
final double res = time / 60;
|
||||||
@ -208,18 +208,16 @@ class Utils {
|
|||||||
|
|
||||||
static int findClosestNumber(int target, List<int> numbers) {
|
static int findClosestNumber(int target, List<int> numbers) {
|
||||||
int minDiff = 127;
|
int minDiff = 127;
|
||||||
int closestNumber = 0; // 初始化为0,表示没有找到比目标值小的整数
|
late int closestNumber;
|
||||||
try {
|
try {
|
||||||
for (int number in numbers) {
|
for (int number in numbers) {
|
||||||
if (number < target) {
|
int diff = (number - target).abs();
|
||||||
int diff = target - number; // 计算目标值与当前整数的差值
|
|
||||||
|
|
||||||
if (diff < minDiff) {
|
if (diff < minDiff) {
|
||||||
minDiff = diff;
|
minDiff = diff;
|
||||||
closestNumber = number;
|
closestNumber = number;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
return closestNumber;
|
return closestNumber;
|
||||||
}
|
}
|
||||||
|
|||||||
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