Merge branch 'main' into design
This commit is contained in:
BIN
assets/images/run-pokemon.gif
Normal file
BIN
assets/images/run-pokemon.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
@ -1,4 +1,6 @@
|
|||||||
PODS:
|
PODS:
|
||||||
|
- auto_orientation (0.0.1):
|
||||||
|
- Flutter
|
||||||
- connectivity_plus (0.0.1):
|
- connectivity_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- ReachabilitySwift
|
- ReachabilitySwift
|
||||||
@ -41,6 +43,7 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
|
- auto_orientation (from `.symlinks/plugins/auto_orientation/ios`)
|
||||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
@ -65,6 +68,8 @@ SPEC REPOS:
|
|||||||
- ReachabilitySwift
|
- ReachabilitySwift
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
|
auto_orientation:
|
||||||
|
:path: ".symlinks/plugins/auto_orientation/ios"
|
||||||
connectivity_plus:
|
connectivity_plus:
|
||||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||||
device_info_plus:
|
device_info_plus:
|
||||||
@ -101,6 +106,7 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
|
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
|
auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d
|
||||||
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
|
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
|
||||||
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
|
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
|
||||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||||
|
|||||||
@ -98,10 +98,12 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Box localCache = GStrorage.localCache;
|
Box localCache = GStrorage.localCache;
|
||||||
|
double statusBarHeight = MediaQuery.of(context).padding.top;
|
||||||
double sheetHeight = MediaQuery.of(context).size.height -
|
double sheetHeight = MediaQuery.of(context).size.height -
|
||||||
MediaQuery.of(context).padding.top -
|
MediaQuery.of(context).padding.top -
|
||||||
MediaQuery.of(context).size.width * 9 / 16;
|
MediaQuery.of(context).size.width * 9 / 16;
|
||||||
localCache.put('sheetHeight', sheetHeight);
|
localCache.put('sheetHeight', sheetHeight);
|
||||||
|
localCache.put('statusBarHeight', statusBarHeight);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: FadeTransition(
|
body: FadeTransition(
|
||||||
opacity: _fadeAnimation!,
|
opacity: _fadeAnimation!,
|
||||||
|
|||||||
@ -54,6 +54,8 @@ class VideoDetailController extends GetxController
|
|||||||
RxBool autoPlay = true.obs;
|
RxBool autoPlay = true.obs;
|
||||||
// 视频资源是否有效
|
// 视频资源是否有效
|
||||||
RxBool isEffective = true.obs;
|
RxBool isEffective = true.obs;
|
||||||
|
// 封面图的展示
|
||||||
|
RxBool isShowCover = true.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -74,7 +76,7 @@ class VideoDetailController extends GetxController
|
|||||||
heroTag = Get.arguments['heroTag'];
|
heroTag = Get.arguments['heroTag'];
|
||||||
}
|
}
|
||||||
tabCtr = TabController(length: 2, vsync: this);
|
tabCtr = TabController(length: 2, vsync: this);
|
||||||
queryVideoUrl();
|
// queryVideoUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
showReplyReplyPanel() {
|
showReplyReplyPanel() {
|
||||||
@ -109,21 +111,21 @@ class VideoDetailController extends GetxController
|
|||||||
/// 根据currentVideoQa 重新设置videoUrl
|
/// 根据currentVideoQa 重新设置videoUrl
|
||||||
VideoItem firstVideo =
|
VideoItem firstVideo =
|
||||||
data.dash!.video!.firstWhere((i) => i.id == currentVideoQa.code);
|
data.dash!.video!.firstWhere((i) => i.id == currentVideoQa.code);
|
||||||
String videoUrl = firstVideo.baseUrl!;
|
// String videoUrl = firstVideo.baseUrl!;
|
||||||
|
|
||||||
/// 根据currentAudioQa 重新设置audioUrl
|
/// 根据currentAudioQa 重新设置audioUrl
|
||||||
AudioItem firstAudio =
|
AudioItem firstAudio =
|
||||||
data.dash!.audio!.firstWhere((i) => i.id == currentAudioQa.code);
|
data.dash!.audio!.firstWhere((i) => i.id == currentAudioQa.code);
|
||||||
String audioUrl = firstAudio.baseUrl ?? '';
|
String audioUrl = firstAudio.baseUrl ?? '';
|
||||||
|
|
||||||
playerInit(videoUrl, audioUrl, defaultST: position);
|
playerInit(firstVideo, audioUrl, defaultST: position);
|
||||||
}
|
}
|
||||||
|
|
||||||
playerInit(source, audioSource,
|
Future playerInit(firstVideo, audioSource,
|
||||||
{Duration defaultST = Duration.zero, int duration = 0}) async {
|
{Duration defaultST = Duration.zero, int duration = 0}) async {
|
||||||
plPlayerController.setDataSource(
|
await plPlayerController.setDataSource(
|
||||||
DataSource(
|
DataSource(
|
||||||
videoSource: source,
|
videoSource: firstVideo.baseUrl,
|
||||||
audioSource: audioSource,
|
audioSource: audioSource,
|
||||||
type: DataSourceType.network,
|
type: DataSourceType.network,
|
||||||
httpHeaders: {
|
httpHeaders: {
|
||||||
@ -137,6 +139,9 @@ class VideoDetailController extends GetxController
|
|||||||
autoplay: autoPlay.value,
|
autoplay: autoPlay.value,
|
||||||
seekTo: defaultST,
|
seekTo: defaultST,
|
||||||
duration: Duration(milliseconds: duration),
|
duration: Duration(milliseconds: duration),
|
||||||
|
// 宽>高 水平 否则 垂直
|
||||||
|
direction:
|
||||||
|
firstVideo.width - firstVideo.height > 0 ? 'horizontal' : 'vertical',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,14 +151,14 @@ class VideoDetailController extends GetxController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 视频链接
|
// 视频链接
|
||||||
queryVideoUrl() async {
|
Future queryVideoUrl() async {
|
||||||
var result = await VideoHttp.videoUrl(cid: cid, bvid: bvid);
|
var result = await VideoHttp.videoUrl(cid: cid, bvid: bvid);
|
||||||
if (result['status']) {
|
if (result['status']) {
|
||||||
data = result['data'];
|
data = result['data'];
|
||||||
|
|
||||||
/// 优先顺序 省流模式 -> 设置中指定质量 -> 当前可选的最高质量
|
/// 优先顺序 省流模式 -> 设置中指定质量 -> 当前可选的最高质量
|
||||||
VideoItem firstVideo = data.dash!.video!.first;
|
VideoItem firstVideo = data.dash!.video!.first;
|
||||||
String videoUrl = firstVideo.baseUrl!;
|
// String videoUrl = firstVideo.baseUrl!;
|
||||||
//
|
//
|
||||||
currentVideoQa = VideoQualityCode.fromCode(firstVideo.id!)!;
|
currentVideoQa = VideoQualityCode.fromCode(firstVideo.id!)!;
|
||||||
|
|
||||||
@ -162,15 +167,17 @@ class VideoDetailController extends GetxController
|
|||||||
data.dash!.audio!.isNotEmpty ? data.dash!.audio!.first : AudioItem();
|
data.dash!.audio!.isNotEmpty ? data.dash!.audio!.first : AudioItem();
|
||||||
String audioUrl = firstAudio.baseUrl ?? '';
|
String audioUrl = firstAudio.baseUrl ?? '';
|
||||||
//
|
//
|
||||||
currentAudioQa = AudioQualityCode.fromCode(firstAudio.id!)!;
|
if (firstAudio.id != null) {
|
||||||
|
currentAudioQa = AudioQualityCode.fromCode(firstAudio.id!)!;
|
||||||
playerInit(
|
}
|
||||||
videoUrl,
|
await playerInit(
|
||||||
|
firstVideo,
|
||||||
audioUrl,
|
audioUrl,
|
||||||
defaultST: Duration(milliseconds: data.lastPlayTime!),
|
defaultST: Duration(milliseconds: data.lastPlayTime!),
|
||||||
duration: data.timeLength ?? 0,
|
duration: data.timeLength ?? 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void loopHeartBeat() {
|
void loopHeartBeat() {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import 'dart:async';
|
|||||||
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||||
import 'package:pilipala/common/widgets/sliver_header.dart';
|
import 'package:pilipala/common/widgets/sliver_header.dart';
|
||||||
import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart';
|
import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart';
|
||||||
@ -11,6 +12,7 @@ import 'package:pilipala/pages/video/detail/controller.dart';
|
|||||||
import 'package:pilipala/pages/video/detail/introduction/index.dart';
|
import 'package:pilipala/pages/video/detail/introduction/index.dart';
|
||||||
import 'package:pilipala/pages/video/detail/related/index.dart';
|
import 'package:pilipala/pages/video/detail/related/index.dart';
|
||||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||||
|
import 'package:pilipala/utils/storage.dart';
|
||||||
|
|
||||||
import 'widgets/app_bar.dart';
|
import 'widgets/app_bar.dart';
|
||||||
import 'widgets/header_control.dart';
|
import 'widgets/header_control.dart';
|
||||||
@ -32,11 +34,15 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
final ScrollController _extendNestCtr = ScrollController();
|
final ScrollController _extendNestCtr = ScrollController();
|
||||||
late StreamController<double> appbarStream;
|
late StreamController<double> appbarStream;
|
||||||
|
|
||||||
bool isPlay = false;
|
|
||||||
PlayerStatus playerStatus = PlayerStatus.playing;
|
PlayerStatus playerStatus = PlayerStatus.playing;
|
||||||
bool isShowCover = true;
|
// bool isShowCover = true;
|
||||||
double doubleOffset = 0;
|
double doubleOffset = 0;
|
||||||
|
|
||||||
|
Box localCache = GStrorage.localCache;
|
||||||
|
late double statusBarHeight;
|
||||||
|
final videoHeight = Get.size.width * 9 / 16;
|
||||||
|
late Future _futureBuilderFuture;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -46,19 +52,16 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
videoDetailController.markHeartBeat();
|
videoDetailController.markHeartBeat();
|
||||||
playerStatus = status;
|
playerStatus = status;
|
||||||
if (status == PlayerStatus.playing) {
|
if (status == PlayerStatus.playing) {
|
||||||
isPlay = false;
|
videoDetailController.isShowCover.value = false;
|
||||||
isShowCover = false;
|
|
||||||
setState(() {});
|
|
||||||
videoDetailController.loopHeartBeat();
|
videoDetailController.loopHeartBeat();
|
||||||
} else {
|
} else {
|
||||||
videoDetailController.timer!.cancel();
|
videoDetailController.timer!.cancel();
|
||||||
isPlay = true;
|
|
||||||
setState(() {});
|
|
||||||
// 播放完成停止 or 切换下一个
|
// 播放完成停止 or 切换下一个
|
||||||
if (status == PlayerStatus.completed) {
|
if (status == PlayerStatus.completed) {
|
||||||
// 当只有1p或多p未打开自动播放时,播放完成还原进度条,展示控制栏
|
// 当只有1p或多p未打开自动播放时,播放完成还原进度条,展示控制栏
|
||||||
plPlayerController!.seekTo(Duration.zero);
|
plPlayerController!.seekTo(Duration.zero);
|
||||||
plPlayerController!.onLockControl(false);
|
plPlayerController!.onLockControl(false);
|
||||||
|
plPlayerController!.videoPlayerController!.pause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -72,6 +75,9 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
appbarStream.add(offset);
|
appbarStream.add(offset);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
statusBarHeight = localCache.get('statusBarHeight');
|
||||||
|
_futureBuilderFuture = videoDetailController.queryVideoUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
void continuePlay() async {
|
void continuePlay() async {
|
||||||
@ -120,7 +126,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final double statusBarHeight = MediaQuery.of(context).padding.top;
|
|
||||||
final videoHeight = MediaQuery.of(context).size.width * 9 / 16;
|
final videoHeight = MediaQuery.of(context).size.width * 9 / 16;
|
||||||
final double pinnedHeaderHeight =
|
final double pinnedHeaderHeight =
|
||||||
statusBarHeight + kToolbarHeight + videoHeight;
|
statusBarHeight + kToolbarHeight + videoHeight;
|
||||||
@ -132,7 +137,9 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
key: videoDetailController.scaffoldKey,
|
key: videoDetailController.scaffoldKey,
|
||||||
backgroundColor: Colors.transparent,
|
// fix 1px black line
|
||||||
|
// backgroundColor: Colors.transparent,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.background,
|
||||||
body: ExtendedNestedScrollView(
|
body: ExtendedNestedScrollView(
|
||||||
controller: _extendNestCtr,
|
controller: _extendNestCtr,
|
||||||
headerSliverBuilder:
|
headerSliverBuilder:
|
||||||
@ -149,8 +156,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
backgroundColor: Theme.of(context).colorScheme.background,
|
backgroundColor: Theme.of(context).colorScheme.background,
|
||||||
flexibleSpace: FlexibleSpaceBar(
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
background: Padding(
|
background: Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(top: statusBarHeight),
|
||||||
top: MediaQuery.of(context).padding.top),
|
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, boxConstraints) {
|
builder: (context, boxConstraints) {
|
||||||
double maxWidth = boxConstraints.maxWidth;
|
double maxWidth = boxConstraints.maxWidth;
|
||||||
@ -159,28 +165,39 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
tag: videoDetailController.heroTag,
|
tag: videoDetailController.heroTag,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
if (plPlayerController!
|
FutureBuilder(
|
||||||
.videoPlayerController !=
|
future: _futureBuilderFuture,
|
||||||
null)
|
builder: ((context, snapshot) {
|
||||||
PLVideoPlayer(
|
if (snapshot.hasData &&
|
||||||
controller: plPlayerController!,
|
snapshot.data['status']) {
|
||||||
headerControl: HeaderControl(
|
return PLVideoPlayer(
|
||||||
controller: plPlayerController,
|
controller: plPlayerController!,
|
||||||
videoDetailCtr: videoDetailController,
|
headerControl: HeaderControl(
|
||||||
),
|
controller: plPlayerController,
|
||||||
),
|
videoDetailCtr:
|
||||||
Visibility(
|
videoDetailController,
|
||||||
visible: isShowCover,
|
),
|
||||||
child: Positioned(
|
);
|
||||||
top: 0,
|
} else {
|
||||||
left: 0,
|
return const SizedBox();
|
||||||
right: 0,
|
}
|
||||||
child: NetworkImgLayer(
|
}),
|
||||||
type: 'emote',
|
),
|
||||||
src: videoDetailController
|
Obx(
|
||||||
.videoItem['pic'],
|
() => Visibility(
|
||||||
width: maxWidth,
|
visible: videoDetailController
|
||||||
height: maxHeight,
|
.isShowCover.value,
|
||||||
|
child: Positioned(
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: NetworkImgLayer(
|
||||||
|
type: 'emote',
|
||||||
|
src: videoDetailController
|
||||||
|
.videoItem['pic'],
|
||||||
|
width: maxWidth,
|
||||||
|
height: maxHeight,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -188,33 +205,58 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
/// 关闭自动播放时 手动播放
|
/// 关闭自动播放时 手动播放
|
||||||
Obx(
|
Obx(
|
||||||
() => Visibility(
|
() => Visibility(
|
||||||
visible: isShowCover &&
|
visible: videoDetailController
|
||||||
videoDetailController
|
.isShowCover.value &&
|
||||||
.isEffective.value &&
|
videoDetailController
|
||||||
!videoDetailController.autoPlay.value,
|
.isEffective.value &&
|
||||||
child: Positioned(
|
!videoDetailController
|
||||||
right: 12,
|
.autoPlay.value,
|
||||||
bottom: 6,
|
child: Stack(
|
||||||
child: TextButton.icon(
|
children: [
|
||||||
style: ButtonStyle(
|
Positioned(
|
||||||
backgroundColor:
|
top: 0,
|
||||||
MaterialStateProperty
|
left: 0,
|
||||||
.resolveWith((states) {
|
right: 0,
|
||||||
return Theme.of(context)
|
child: AppBar(
|
||||||
.colorScheme
|
primary: false,
|
||||||
.primaryContainer;
|
backgroundColor:
|
||||||
}),
|
Colors.transparent,
|
||||||
),
|
actions: [
|
||||||
onPressed: () => videoDetailController
|
/// TODO
|
||||||
.handlePlay(),
|
IconButton(
|
||||||
icon: const Icon(
|
tooltip: '稍后再看',
|
||||||
Icons.play_circle_outline,
|
onPressed: () {},
|
||||||
size: 20,
|
icon: const Icon(Icons
|
||||||
),
|
.history_outlined))
|
||||||
label: const Text('Play'),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Positioned(
|
||||||
|
right: 12,
|
||||||
|
bottom: 6,
|
||||||
|
child: TextButton.icon(
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor:
|
||||||
|
MaterialStateProperty
|
||||||
|
.resolveWith(
|
||||||
|
(states) {
|
||||||
|
return Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primaryContainer;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
onPressed: () =>
|
||||||
|
videoDetailController
|
||||||
|
.handlePlay(),
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.play_circle_outline,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
label: const Text('Play'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/painting.dart';
|
import 'package:flutter/painting.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
@ -53,6 +54,9 @@ class PlPlayerController {
|
|||||||
final Rx<bool> _showBrightnessStatus = false.obs;
|
final Rx<bool> _showBrightnessStatus = false.obs;
|
||||||
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<String> _direction = 'horizontal'.obs;
|
||||||
|
|
||||||
Rx<bool> videoFitChanged = false.obs;
|
Rx<bool> videoFitChanged = false.obs;
|
||||||
final Rx<BoxFit> _videoFit = Rx(BoxFit.fill);
|
final Rx<BoxFit> _videoFit = Rx(BoxFit.fill);
|
||||||
@ -82,6 +86,8 @@ class PlPlayerController {
|
|||||||
BoxFit.scaleDown
|
BoxFit.scaleDown
|
||||||
];
|
];
|
||||||
|
|
||||||
|
PreferredSizeWidget? headerControl;
|
||||||
|
|
||||||
/// 数据加载监听
|
/// 数据加载监听
|
||||||
Stream<DataStatus> get onDataStatusChanged => dataStatus.status.stream;
|
Stream<DataStatus> get onDataStatusChanged => dataStatus.status.stream;
|
||||||
|
|
||||||
@ -160,6 +166,12 @@ class PlPlayerController {
|
|||||||
/// 屏幕锁 为true时,关闭控制栏
|
/// 屏幕锁 为true时,关闭控制栏
|
||||||
Rx<bool> get controlsLock => _controlsLock;
|
Rx<bool> get controlsLock => _controlsLock;
|
||||||
|
|
||||||
|
/// 全屏状态
|
||||||
|
Rx<bool> get isFullScreen => _isFullScreen;
|
||||||
|
|
||||||
|
/// 全屏方向
|
||||||
|
Rx<String> get direction => _direction;
|
||||||
|
|
||||||
PlPlayerController({
|
PlPlayerController({
|
||||||
// 直播间 传false 关闭控制栏
|
// 直播间 传false 关闭控制栏
|
||||||
this.controlsEnabled = true,
|
this.controlsEnabled = true,
|
||||||
@ -197,6 +209,9 @@ class PlPlayerController {
|
|||||||
double? width,
|
double? width,
|
||||||
double? height,
|
double? height,
|
||||||
Duration? duration,
|
Duration? duration,
|
||||||
|
// 方向
|
||||||
|
String? direction,
|
||||||
|
// 全屏模式
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
_autoPlay = autoplay;
|
_autoPlay = autoplay;
|
||||||
@ -207,6 +222,8 @@ class PlPlayerController {
|
|||||||
_playbackSpeed.value = speed;
|
_playbackSpeed.value = speed;
|
||||||
// 初始化数据加载状态
|
// 初始化数据加载状态
|
||||||
dataStatus.status.value = DataStatus.loading;
|
dataStatus.status.value = DataStatus.loading;
|
||||||
|
// 初始化全屏方向
|
||||||
|
_direction.value = direction ?? 'horizontal';
|
||||||
|
|
||||||
if (_videoPlayerController != null &&
|
if (_videoPlayerController != null &&
|
||||||
_videoPlayerController!.state.playing) {
|
_videoPlayerController!.state.playing) {
|
||||||
@ -624,6 +641,10 @@ class PlPlayerController {
|
|||||||
showControls.value = !val;
|
showControls.value = !val;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void toggleFullScreen(bool val) {
|
||||||
|
_isFullScreen.value = val;
|
||||||
|
}
|
||||||
|
|
||||||
/// 截屏
|
/// 截屏
|
||||||
Future screenshot() async {
|
Future screenshot() async {
|
||||||
final Uint8List? screenshot =
|
final Uint8List? screenshot =
|
||||||
|
|||||||
@ -7,3 +7,6 @@ export './models/play_status.dart';
|
|||||||
export './models/data_status.dart';
|
export './models/data_status.dart';
|
||||||
export './widgets/common_btn.dart';
|
export './widgets/common_btn.dart';
|
||||||
export './models/play_speed.dart';
|
export './models/play_speed.dart';
|
||||||
|
export './widgets/app_bar_ani.dart';
|
||||||
|
export './utils/fullscreen.dart';
|
||||||
|
export './utils.dart';
|
||||||
|
|||||||
9
lib/plugin/pl_player/models/fullscreen_mode.dart
Normal file
9
lib/plugin/pl_player/models/fullscreen_mode.dart
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// 全屏模式
|
||||||
|
enum FullScreenMode {
|
||||||
|
// 根据内容自适应
|
||||||
|
auto,
|
||||||
|
// 始终竖屏
|
||||||
|
vertical,
|
||||||
|
// 始终横屏
|
||||||
|
horizontal
|
||||||
|
}
|
||||||
40
lib/plugin/pl_player/utils/fullscreen.dart
Normal file
40
lib/plugin/pl_player/utils/fullscreen.dart
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
|
import 'package:auto_orientation/auto_orientation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
//横屏
|
||||||
|
/// 低版本xcode不支持auto_orientation
|
||||||
|
Future<void> landScape() async {
|
||||||
|
if (Platform.isAndroid || Platform.isIOS) {
|
||||||
|
await AutoOrientation.landscapeAutoMode(forceSensor: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//竖屏
|
||||||
|
Future<void> verticalScreen() async {
|
||||||
|
await SystemChrome.setPreferredOrientations([
|
||||||
|
DeviceOrientation.portraitUp,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> enterFullScreen() async {
|
||||||
|
await SystemChrome.setEnabledSystemUIMode(
|
||||||
|
SystemUiMode.immersiveSticky,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//退出全屏显示
|
||||||
|
Future<void> exitFullScreen() async {
|
||||||
|
late SystemUiMode mode;
|
||||||
|
if ((Platform.isAndroid &&
|
||||||
|
(await DeviceInfoPlugin().androidInfo).version.sdkInt >= 29) ||
|
||||||
|
!Platform.isAndroid) {
|
||||||
|
mode = SystemUiMode.edgeToEdge;
|
||||||
|
} else {
|
||||||
|
mode = SystemUiMode.manual;
|
||||||
|
}
|
||||||
|
await SystemChrome.setEnabledSystemUIMode(mode,
|
||||||
|
overlays: [SystemUiOverlay.top, SystemUiOverlay.bottom]);
|
||||||
|
}
|
||||||
@ -1,5 +1,8 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
|
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart';
|
||||||
@ -10,12 +13,14 @@ import 'package:pilipala/plugin/pl_player/models/duration.dart';
|
|||||||
import 'package:pilipala/plugin/pl_player/models/play_status.dart';
|
import 'package:pilipala/plugin/pl_player/models/play_status.dart';
|
||||||
import 'package:pilipala/plugin/pl_player/utils.dart';
|
import 'package:pilipala/plugin/pl_player/utils.dart';
|
||||||
import 'package:pilipala/utils/feed_back.dart';
|
import 'package:pilipala/utils/feed_back.dart';
|
||||||
|
import 'package:screen_brightness/screen_brightness.dart';
|
||||||
|
import 'package:volume_controller/volume_controller.dart';
|
||||||
|
|
||||||
|
import 'utils/fullscreen.dart';
|
||||||
import 'widgets/backward_seek.dart';
|
import 'widgets/backward_seek.dart';
|
||||||
import 'widgets/bottom_control.dart';
|
import 'widgets/bottom_control.dart';
|
||||||
import 'widgets/common_btn.dart';
|
import 'widgets/common_btn.dart';
|
||||||
import 'widgets/forward_seek.dart';
|
import 'widgets/forward_seek.dart';
|
||||||
import 'widgets/play_pause_btn.dart';
|
|
||||||
|
|
||||||
class PLVideoPlayer extends StatefulWidget {
|
class PLVideoPlayer extends StatefulWidget {
|
||||||
final PlPlayerController controller;
|
final PlPlayerController controller;
|
||||||
@ -43,6 +48,18 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
bool _hideSeekBackwardButton = false;
|
bool _hideSeekBackwardButton = false;
|
||||||
bool _hideSeekForwardButton = false;
|
bool _hideSeekForwardButton = false;
|
||||||
|
|
||||||
|
double _brightnessValue = 0.0;
|
||||||
|
bool _brightnessIndicator = false;
|
||||||
|
Timer? _brightnessTimer;
|
||||||
|
|
||||||
|
double _volumeValue = 0.0;
|
||||||
|
bool _volumeIndicator = false;
|
||||||
|
Timer? _volumeTimer;
|
||||||
|
|
||||||
|
double _distance = 0.0;
|
||||||
|
|
||||||
|
bool _volumeInterceptEventStream = false;
|
||||||
|
|
||||||
void onDoubleTapSeekBackward() {
|
void onDoubleTapSeekBackward() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_mountSeekBackwardButton = true;
|
_mountSeekBackwardButton = true;
|
||||||
@ -61,12 +78,128 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
animationController = AnimationController(
|
animationController = AnimationController(
|
||||||
vsync: this, duration: const Duration(milliseconds: 300));
|
vsync: this, duration: const Duration(milliseconds: 300));
|
||||||
videoController = widget.controller.videoController!;
|
videoController = widget.controller.videoController!;
|
||||||
|
widget.controller.headerControl = widget.headerControl;
|
||||||
|
|
||||||
|
Future.microtask(() async {
|
||||||
|
try {
|
||||||
|
VolumeController().showSystemUI = false;
|
||||||
|
_volumeValue = await VolumeController().getVolume();
|
||||||
|
VolumeController().listener((value) {
|
||||||
|
if (mounted && !_volumeInterceptEventStream) {
|
||||||
|
setState(() {
|
||||||
|
_volumeValue = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (_) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
Future.microtask(() async {
|
||||||
|
try {
|
||||||
|
_brightnessValue = await ScreenBrightness().current;
|
||||||
|
ScreenBrightness().onCurrentBrightnessChanged.listen((value) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_brightnessValue = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (_) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setVolume(double value) async {
|
||||||
|
try {
|
||||||
|
VolumeController().setVolume(value);
|
||||||
|
} catch (_) {}
|
||||||
|
setState(() {
|
||||||
|
_volumeValue = value;
|
||||||
|
_volumeIndicator = true;
|
||||||
|
_volumeInterceptEventStream = true;
|
||||||
|
});
|
||||||
|
_volumeTimer?.cancel();
|
||||||
|
_volumeTimer = Timer(const Duration(milliseconds: 200), () {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_volumeIndicator = false;
|
||||||
|
_volumeInterceptEventStream = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setBrightness(double value) async {
|
||||||
|
try {
|
||||||
|
await ScreenBrightness().setScreenBrightness(value);
|
||||||
|
} catch (_) {}
|
||||||
|
setState(() {
|
||||||
|
_brightnessIndicator = true;
|
||||||
|
});
|
||||||
|
_brightnessTimer?.cancel();
|
||||||
|
_brightnessTimer = Timer(const Duration(milliseconds: 200), () {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_brightnessIndicator = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> triggerFullScreen() async {
|
||||||
|
PlPlayerController _ = widget.controller;
|
||||||
|
if (!_.isFullScreen.value) {
|
||||||
|
/// 按照视频宽高比决定全屏方向
|
||||||
|
if (_.direction.value == 'horizontal') {
|
||||||
|
/// 进入全屏
|
||||||
|
await enterFullScreen();
|
||||||
|
// 横屏
|
||||||
|
await landScape();
|
||||||
|
} else {
|
||||||
|
// 竖屏
|
||||||
|
await verticalScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
_.toggleFullScreen(true);
|
||||||
|
var result = await showDialog(
|
||||||
|
context: Get.context!,
|
||||||
|
useSafeArea: false,
|
||||||
|
builder: (context) => Dialog.fullscreen(
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
appBar: AppBar(
|
||||||
|
primary: false,
|
||||||
|
toolbarHeight: 0,
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
systemOverlayStyle: SystemUiOverlayStyle.light,
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
bottom: true,
|
||||||
|
child: PLVideoPlayer(
|
||||||
|
controller: _,
|
||||||
|
headerControl: _.headerControl,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (result == null) {
|
||||||
|
// 退出全屏
|
||||||
|
exitFullScreen();
|
||||||
|
await verticalScreen();
|
||||||
|
_.toggleFullScreen(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Get.back();
|
||||||
|
exitFullScreen();
|
||||||
|
await verticalScreen();
|
||||||
|
_.toggleFullScreen(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
|
||||||
animationController.dispose();
|
animationController.dispose();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -90,25 +223,6 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
fit: StackFit.passthrough,
|
fit: StackFit.passthrough,
|
||||||
children: [
|
children: [
|
||||||
// Wrap [Video] widget with [MaterialVideoControlsTheme].
|
|
||||||
// MaterialVideoControlsTheme(
|
|
||||||
// normal: MaterialVideoControlsThemeData(
|
|
||||||
// // Modify theme options:
|
|
||||||
// buttonBarButtonSize: 24.0,
|
|
||||||
// buttonBarButtonColor: Colors.white,
|
|
||||||
// ),
|
|
||||||
// fullscreen: const MaterialVideoControlsThemeData(
|
|
||||||
// // Modify theme options:
|
|
||||||
// displaySeekBar: false,
|
|
||||||
// automaticallyImplySkipNextButton: false,
|
|
||||||
// automaticallyImplySkipPreviousButton: false,
|
|
||||||
// ),
|
|
||||||
// child: Scaffold(
|
|
||||||
// body: Video(
|
|
||||||
// controller: videoController,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
Video(
|
Video(
|
||||||
controller: videoController,
|
controller: videoController,
|
||||||
controls: NoVideoControls,
|
controls: NoVideoControls,
|
||||||
@ -118,69 +232,295 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 20, bottom: 15),
|
/// 长按倍速
|
||||||
child: GestureDetector(
|
Obx(
|
||||||
onTap: () {
|
() => Align(
|
||||||
_.controls = !_.showControls.value;
|
alignment: Alignment.topCenter,
|
||||||
},
|
child: FractionalTranslation(
|
||||||
// onDoubleTap: () {
|
translation: const Offset(0.0, 1), // 上下偏移量(负数向上偏移)
|
||||||
// if (_.playerStatus.status.value == PlayerStatus.playing) {
|
child: AnimatedOpacity(
|
||||||
// _.togglePlay();
|
curve: Curves.easeInOut,
|
||||||
// } else {
|
opacity: _.doubleSpeedStatus.value ? 1.0 : 0.0,
|
||||||
// _.play();
|
duration: const Duration(milliseconds: 150),
|
||||||
// }
|
child: Container(
|
||||||
// },
|
alignment: Alignment.center,
|
||||||
onDoubleTapDown: (details) {
|
decoration: BoxDecoration(
|
||||||
final totalWidth = MediaQuery.of(context).size.width;
|
color: const Color(0x88000000),
|
||||||
final tapPosition = details.localPosition.dx;
|
borderRadius: BorderRadius.circular(64.0),
|
||||||
final sectionWidth = totalWidth / 3;
|
),
|
||||||
if (tapPosition < sectionWidth) {
|
height: 34.0,
|
||||||
// 双击左边区域 👈
|
width: 86.0,
|
||||||
onDoubleTapSeekBackward();
|
child: Row(
|
||||||
} else if (tapPosition < sectionWidth * 2) {
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
if (_.playerStatus.status.value == PlayerStatus.playing) {
|
children: [
|
||||||
_.togglePlay();
|
const SizedBox(width: 3),
|
||||||
} else {
|
Image.asset(
|
||||||
_.play();
|
'assets/images/run-pokemon.gif',
|
||||||
}
|
height: 20,
|
||||||
} else {
|
),
|
||||||
// 双击右边区域 👈
|
const Text(
|
||||||
onDoubleTapSeekForward();
|
'倍速中',
|
||||||
}
|
style: TextStyle(color: Colors.white, fontSize: 12),
|
||||||
},
|
),
|
||||||
onLongPressStart: (detail) {
|
const SizedBox(width: 4),
|
||||||
feedBack();
|
],
|
||||||
double currentSpeed = _.playbackSpeed;
|
),
|
||||||
_.setDoubleSpeedStatus(true);
|
),
|
||||||
_.setPlaybackSpeed(currentSpeed * 2);
|
),
|
||||||
},
|
),
|
||||||
onLongPressEnd: (details) {
|
),
|
||||||
double currentSpeed = _.playbackSpeed;
|
|
||||||
_.setDoubleSpeedStatus(false);
|
|
||||||
_.setPlaybackSpeed(currentSpeed / 2);
|
|
||||||
},
|
|
||||||
// 水平位置 快进
|
|
||||||
onHorizontalDragUpdate: (DragUpdateDetails details) {},
|
|
||||||
onHorizontalDragEnd: (DragEndDetails details) {},
|
|
||||||
// 垂直方向 音量/亮度调节
|
|
||||||
onVerticalDragUpdate: (DragUpdateDetails details) {},
|
|
||||||
onVerticalDragEnd: (DragEndDetails details) {}),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
|
/// 时间进度
|
||||||
|
Obx(
|
||||||
|
() => Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: FractionalTranslation(
|
||||||
|
translation: const Offset(0.0, 1.0), // 上下偏移量(负数向上偏移)
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
opacity: _.isSliderMoving.value ? 1.0 : 0.0,
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
child: Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0x88000000),
|
||||||
|
borderRadius: BorderRadius.circular(64.0),
|
||||||
|
),
|
||||||
|
height: 34.0,
|
||||||
|
width: 100.0,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Obx(() {
|
||||||
|
return Text(
|
||||||
|
_.sliderTempPosition.value.inMinutes >= 60
|
||||||
|
? printDurationWithHours(
|
||||||
|
_.sliderTempPosition.value)
|
||||||
|
: printDuration(_.sliderTempPosition.value),
|
||||||
|
style: textStyle,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
const Text('/', style: textStyle),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
Obx(
|
||||||
|
() => Text(
|
||||||
|
_.duration.value.inMinutes >= 60
|
||||||
|
? printDurationWithHours(_.duration.value)
|
||||||
|
: printDuration(_.duration.value),
|
||||||
|
style: textStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// 音量🔊 控制条展示
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
opacity: _volumeIndicator ? 1.0 : 0.0,
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
child: Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0x88000000),
|
||||||
|
borderRadius: BorderRadius.circular(64.0),
|
||||||
|
),
|
||||||
|
height: 34.0,
|
||||||
|
width: 70.0,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 34.0,
|
||||||
|
width: 28.0,
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Icon(
|
||||||
|
_volumeValue == 0.0
|
||||||
|
? Icons.volume_off
|
||||||
|
: _volumeValue < 0.5
|
||||||
|
? Icons.volume_down
|
||||||
|
: Icons.volume_up,
|
||||||
|
color: const Color(0xFFFFFFFF),
|
||||||
|
size: 20.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'${(_volumeValue * 100.0).round()}%',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13.0,
|
||||||
|
color: Color(0xFFFFFFFF),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6.0),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// 亮度🌞 控制条展示
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
opacity: _brightnessIndicator ? 1.0 : 0.0,
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
child: Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0x88000000),
|
||||||
|
borderRadius: BorderRadius.circular(64.0),
|
||||||
|
),
|
||||||
|
height: 34.0,
|
||||||
|
width: 70.0,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 30.0,
|
||||||
|
width: 28.0,
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Icon(
|
||||||
|
_brightnessValue < 1.0 / 3.0
|
||||||
|
? Icons.brightness_low
|
||||||
|
: _brightnessValue < 2.0 / 3.0
|
||||||
|
? Icons.brightness_medium
|
||||||
|
: Icons.brightness_high,
|
||||||
|
color: const Color(0xFFFFFFFF),
|
||||||
|
size: 18.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 2.0),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'${(_brightnessValue * 100.0).round()}%',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13.0,
|
||||||
|
color: Color(0xFFFFFFFF),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6.0),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// 手势
|
||||||
|
Positioned.fill(
|
||||||
|
left: 16,
|
||||||
|
top: 25,
|
||||||
|
right: 15,
|
||||||
|
bottom: 15,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
_.controls = !_.showControls.value;
|
||||||
|
},
|
||||||
|
onDoubleTapDown: (details) {
|
||||||
|
final totalWidth = MediaQuery.of(context).size.width;
|
||||||
|
final tapPosition = details.localPosition.dx;
|
||||||
|
final sectionWidth = totalWidth / 3;
|
||||||
|
if (tapPosition < sectionWidth) {
|
||||||
|
// 双击左边区域 👈
|
||||||
|
onDoubleTapSeekBackward();
|
||||||
|
} else if (tapPosition < sectionWidth * 2) {
|
||||||
|
if (_.playerStatus.status.value == PlayerStatus.playing) {
|
||||||
|
_.togglePlay();
|
||||||
|
} else {
|
||||||
|
_.play();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 双击右边区域 👈
|
||||||
|
onDoubleTapSeekForward();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongPressStart: (detail) {
|
||||||
|
feedBack();
|
||||||
|
double currentSpeed = _.playbackSpeed;
|
||||||
|
_.setDoubleSpeedStatus(true);
|
||||||
|
_.setPlaybackSpeed(currentSpeed * 2);
|
||||||
|
},
|
||||||
|
onLongPressEnd: (details) {
|
||||||
|
double currentSpeed = _.playbackSpeed;
|
||||||
|
_.setDoubleSpeedStatus(false);
|
||||||
|
_.setPlaybackSpeed(currentSpeed / 2);
|
||||||
|
},
|
||||||
|
// 水平位置 快进
|
||||||
|
onHorizontalDragUpdate: (DragUpdateDetails details) {},
|
||||||
|
onHorizontalDragEnd: (DragEndDetails details) {},
|
||||||
|
// 垂直方向 音量/亮度调节
|
||||||
|
onVerticalDragUpdate: (DragUpdateDetails details) async {
|
||||||
|
final totalWidth = MediaQuery.of(context).size.width;
|
||||||
|
final tapPosition = details.localPosition.dx;
|
||||||
|
final sectionWidth = totalWidth / 3;
|
||||||
|
final delta = details.delta.dy;
|
||||||
|
if (tapPosition < sectionWidth) {
|
||||||
|
// 左边区域 👈
|
||||||
|
final brightness = _brightnessValue - delta / 100.0;
|
||||||
|
final result = brightness.clamp(0.0, 1.0);
|
||||||
|
setBrightness(result);
|
||||||
|
} else if (tapPosition < sectionWidth * 2) {
|
||||||
|
// 全屏
|
||||||
|
final double dy = details.delta.dy;
|
||||||
|
const double threshold = 7.0; // 滑动阈值
|
||||||
|
if (dy > _distance && dy > threshold) {
|
||||||
|
if (!_.isFullScreen.value) {
|
||||||
|
await triggerFullScreen();
|
||||||
|
}
|
||||||
|
_distance = 0.0;
|
||||||
|
} else if (dy < _distance && dy < -threshold) {
|
||||||
|
if (_.isFullScreen.value) {
|
||||||
|
await triggerFullScreen();
|
||||||
|
}
|
||||||
|
_distance = 0.0;
|
||||||
|
}
|
||||||
|
_distance = dy;
|
||||||
|
|
||||||
|
// triggerFullScreen();
|
||||||
|
} else {
|
||||||
|
// 右边区域 👈
|
||||||
|
final volume = _volumeValue - delta / 100.0;
|
||||||
|
final result = volume.clamp(0.0, 1.0);
|
||||||
|
setVolume(result);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onVerticalDragEnd: (DragEndDetails details) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// 头部、底部控制条
|
// 头部、底部控制条
|
||||||
if (_.controlsEnabled)
|
if (_.controlsEnabled)
|
||||||
Obx(
|
Obx(
|
||||||
() => Column(
|
() => Column(
|
||||||
children: [
|
children: [
|
||||||
ClipRect(
|
if (widget.headerControl != null)
|
||||||
clipBehavior: Clip.hardEdge,
|
ClipRect(
|
||||||
child: AppBarAni(
|
clipBehavior: Clip.hardEdge,
|
||||||
controller: animationController,
|
child: AppBarAni(
|
||||||
visible: !_.controlsLock.value && _.showControls.value,
|
controller: animationController,
|
||||||
position: 'top',
|
visible: !_.controlsLock.value && _.showControls.value,
|
||||||
child: widget.headerControl!,
|
position: 'top',
|
||||||
|
child: widget.headerControl!,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
ClipRect(
|
ClipRect(
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
@ -188,7 +528,9 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
controller: animationController,
|
controller: animationController,
|
||||||
visible: !_.controlsLock.value && _.showControls.value,
|
visible: !_.controlsLock.value && _.showControls.value,
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
child: BottomControl(controller: widget.controller),
|
child: BottomControl(
|
||||||
|
controller: widget.controller,
|
||||||
|
triggerFullScreen: triggerFullScreen),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -245,26 +587,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
// 长按倍速
|
|
||||||
Obx(
|
|
||||||
() => Align(
|
|
||||||
alignment: Alignment.topCenter,
|
|
||||||
child: FractionalTranslation(
|
|
||||||
translation: const Offset(0.0, 1.5), // 上下偏移量(负数向上偏移)
|
|
||||||
child: Visibility(
|
|
||||||
visible: _.doubleSpeedStatus.value,
|
|
||||||
child: const Text(
|
|
||||||
'** 倍速中 **',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
backgroundColor: Color(0xaa000000),
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 锁
|
// 锁
|
||||||
if (_.controlsEnabled)
|
if (_.controlsEnabled)
|
||||||
Obx(
|
Obx(
|
||||||
@ -301,44 +624,8 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
// 时间进度
|
|
||||||
/// TDDO 样式
|
/// 点击 快进/快退
|
||||||
Obx(
|
|
||||||
() => Align(
|
|
||||||
alignment: Alignment.topCenter,
|
|
||||||
child: FractionalTranslation(
|
|
||||||
translation: const Offset(0.0, 2.5), // 上下偏移量(负数向上偏移)
|
|
||||||
child: Visibility(
|
|
||||||
visible: _.isSliderMoving.value,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Obx(() {
|
|
||||||
return Text(
|
|
||||||
_.sliderTempPosition.value.inMinutes >= 60
|
|
||||||
? printDurationWithHours(_.sliderTempPosition.value)
|
|
||||||
: printDuration(_.sliderTempPosition.value),
|
|
||||||
style: textStyle,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
const SizedBox(width: 2),
|
|
||||||
const Text('/', style: textStyle),
|
|
||||||
const SizedBox(width: 2),
|
|
||||||
Obx(
|
|
||||||
() => Text(
|
|
||||||
_.duration.value.inMinutes >= 60
|
|
||||||
? printDurationWithHours(_.duration.value)
|
|
||||||
: printDuration(_.duration.value),
|
|
||||||
style: textStyle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 点击 快进/快退
|
|
||||||
if (_mountSeekBackwardButton || _mountSeekForwardButton)
|
if (_mountSeekBackwardButton || _mountSeekForwardButton)
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -440,18 +727,3 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MSliderTrackShape extends RoundedRectSliderTrackShape {
|
|
||||||
@override
|
|
||||||
Rect getPreferredRect({
|
|
||||||
required RenderBox parentBox,
|
|
||||||
Offset offset = Offset.zero,
|
|
||||||
SliderThemeData? sliderTheme,
|
|
||||||
bool isEnabled = false,
|
|
||||||
bool isDiscrete = false,
|
|
||||||
}) {
|
|
||||||
final double trackLeft = offset.dx;
|
|
||||||
final double trackWidth = parentBox.size.width;
|
|
||||||
return Rect.fromLTWH(trackLeft, -1, trackWidth, 3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -5,11 +5,11 @@ import 'package:get/get.dart';
|
|||||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||||
import 'package:pilipala/plugin/pl_player/widgets/play_pause_btn.dart';
|
import 'package:pilipala/plugin/pl_player/widgets/play_pause_btn.dart';
|
||||||
|
|
||||||
import '../utils.dart';
|
|
||||||
|
|
||||||
class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
||||||
final PlPlayerController? controller;
|
final PlPlayerController? controller;
|
||||||
const BottomControl({this.controller, Key? key}) : super(key: key);
|
final Function? triggerFullScreen;
|
||||||
|
const BottomControl({this.controller, this.triggerFullScreen, Key? key})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Size get preferredSize => const Size(double.infinity, kToolbarHeight);
|
Size get preferredSize => const Size(double.infinity, kToolbarHeight);
|
||||||
@ -138,13 +138,17 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
// ),
|
// ),
|
||||||
// const SizedBox(width: 4),
|
// const SizedBox(width: 4),
|
||||||
// 全屏
|
// 全屏
|
||||||
ComBtn(
|
Obx(
|
||||||
icon: const Icon(
|
() => ComBtn(
|
||||||
FontAwesomeIcons.expand,
|
icon: Icon(
|
||||||
size: 15,
|
_.isFullScreen.value
|
||||||
color: Colors.white,
|
? FontAwesomeIcons.a
|
||||||
|
: FontAwesomeIcons.expand,
|
||||||
|
size: 15,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
fuc: () => triggerFullScreen!(),
|
||||||
),
|
),
|
||||||
fuc: () => {},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -49,6 +49,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
version: "1.0.1"
|
||||||
|
auto_orientation:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: auto_orientation
|
||||||
|
sha256: cd56bb59b36fa54cc28ee254bc600524f022a4862f31d5ab20abd7bb1c54e678
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.1"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -107,6 +107,7 @@ dependencies:
|
|||||||
universal_platform: ^1.0.0+1
|
universal_platform: ^1.0.0+1
|
||||||
# 进度条
|
# 进度条
|
||||||
audio_video_progress_bar: ^1.0.1
|
audio_video_progress_bar: ^1.0.1
|
||||||
|
auto_orientation: ^2.3.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user