Merge branch 'main' into design

This commit is contained in:
guozhigq
2023-08-03 22:56:54 +08:00
13 changed files with 647 additions and 232 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
// 全屏模式
enum FullScreenMode {
// 根据内容自适应
auto,
// 始终竖屏
vertical,
// 始终横屏
horizontal
}

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

View File

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

View File

@ -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: () => {},
), ),
], ],
), ),

View File

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

View File

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