- 上游: github.com/OpenIMSDK/openim-flutter-demo tag 3.8.3-patch.3 commit b3dfdb1e8aaeaaf6f0793e10cadd20d5c184a31f - 并行目录 mobile-next/ 原样导入(含 LICENSE),不覆盖现网 mobile/ - 锁定 Flutter 3.24.5 / Dart 3.5.4 / JDK 17 / Gradle 7.6.3 / AGP 7.3.1 - 实测 Android debug 与 release 构建均成功(见 docs/mobile-next-baseline-import.md) - 本提交可单独回退:git revert 1db1229(重写前) Co-authored-by: multica-agent <github@multica.ai>
1070 lines
39 KiB
Dart
1070 lines
39 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:media_kit_video/media_kit_video.dart';
|
|
import 'package:volume_controller/volume_controller.dart';
|
|
import 'package:screen_brightness/screen_brightness.dart';
|
|
|
|
import 'package:media_kit_video/media_kit_video_controls/src/controls/methods/video_state.dart';
|
|
import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart';
|
|
import 'package:media_kit_video/media_kit_video_controls/src/controls/widgets/video_controls_theme_data_injector.dart';
|
|
|
|
Widget CustomMKMaterialVideoControls(VideoState state) {
|
|
return const VideoControlsThemeDataInjector(
|
|
child: _MaterialVideoControls(),
|
|
);
|
|
}
|
|
|
|
MaterialVideoControlsThemeData _theme(BuildContext context) => FullscreenInheritedWidget.maybeOf(context) == null
|
|
? MaterialVideoControlsTheme.maybeOf(context)?.normal ?? kDefaultMaterialVideoControlsThemeData
|
|
: MaterialVideoControlsTheme.maybeOf(context)?.fullscreen ?? kDefaultMaterialVideoControlsThemeDataFullscreen;
|
|
|
|
class _CustomMKMaterialVideoControls extends StatefulWidget {
|
|
const _CustomMKMaterialVideoControls({super.key});
|
|
|
|
@override
|
|
State<_CustomMKMaterialVideoControls> createState() => _CustomMMaterialVideoControlsState();
|
|
}
|
|
|
|
class _CustomMMaterialVideoControlsState extends State<_CustomMKMaterialVideoControls> {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container();
|
|
}
|
|
}
|
|
|
|
class _MaterialVideoControls extends StatefulWidget {
|
|
const _MaterialVideoControls();
|
|
|
|
@override
|
|
State<_MaterialVideoControls> createState() => _MaterialVideoControlsState();
|
|
}
|
|
|
|
class _MaterialVideoControlsState extends State<_MaterialVideoControls> {
|
|
late bool mount = _theme(context).visibleOnMount;
|
|
late bool visible = _theme(context).visibleOnMount;
|
|
Timer? _timer;
|
|
|
|
double _brightnessValue = 0.0;
|
|
bool _brightnessIndicator = false;
|
|
Timer? _brightnessTimer;
|
|
|
|
double _volumeValue = 0.0;
|
|
bool _volumeIndicator = false;
|
|
Timer? _volumeTimer;
|
|
|
|
bool _volumeInterceptEventStream = false;
|
|
|
|
Offset _dragInitialDelta = Offset.zero; // Initial position for horizontal drag
|
|
int swipeDuration = 0; // Duration to seek in video
|
|
bool showSwipeDuration = false; // Whether to show the seek duration overlay
|
|
|
|
bool _speedUpIndicator = false;
|
|
late /* private */ var playlist = controller(context).player.state.playlist;
|
|
late bool buffering = controller(context).player.state.buffering;
|
|
|
|
bool _mountSeekBackwardButton = false;
|
|
bool _mountSeekForwardButton = false;
|
|
bool _hideSeekBackwardButton = false;
|
|
bool _hideSeekForwardButton = false;
|
|
Timer? _timerSeekBackwardButton;
|
|
Timer? _timerSeekForwardButton;
|
|
|
|
final ValueNotifier<Duration> _seekBarDeltaValueNotifier = ValueNotifier<Duration>(Duration.zero);
|
|
|
|
final List<StreamSubscription> subscriptions = [];
|
|
|
|
double get subtitleVerticalShiftOffset =>
|
|
(_theme(context).padding?.bottom ?? 0.0) +
|
|
(_theme(context).bottomButtonBarMargin.vertical) +
|
|
(_theme(context).bottomButtonBar.isNotEmpty ? _theme(context).buttonBarHeight : 0.0);
|
|
Offset? _tapPosition;
|
|
|
|
void _handleDoubleTapDown(TapDownDetails details) {
|
|
setState(() {
|
|
_tapPosition = details.localPosition;
|
|
});
|
|
}
|
|
|
|
void _handleLongPress() {
|
|
setState(() {
|
|
_speedUpIndicator = true;
|
|
});
|
|
controller(context).player.setRate(_theme(context).speedUpFactor);
|
|
}
|
|
|
|
void _handleLongPressEnd(LongPressEndDetails details) {
|
|
setState(() {
|
|
_speedUpIndicator = false;
|
|
});
|
|
controller(context).player.setRate(1.0);
|
|
}
|
|
|
|
@override
|
|
void setState(VoidCallback fn) {
|
|
if (mounted) {
|
|
super.setState(fn);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
if (subscriptions.isEmpty) {
|
|
subscriptions.addAll(
|
|
[
|
|
controller(context).player.stream.playlist.listen(
|
|
(event) {
|
|
setState(() {
|
|
playlist = event;
|
|
});
|
|
},
|
|
),
|
|
controller(context).player.stream.buffering.listen(
|
|
(event) {
|
|
setState(() {
|
|
buffering = event;
|
|
});
|
|
},
|
|
),
|
|
],
|
|
);
|
|
|
|
if (_theme(context).visibleOnMount) {
|
|
_timer = Timer(
|
|
_theme(context).controlsHoverDuration,
|
|
() {
|
|
if (mounted) {
|
|
setState(() {
|
|
visible = false;
|
|
});
|
|
unshiftSubtitle();
|
|
}
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
for (final subscription in subscriptions) {
|
|
subscription.cancel();
|
|
}
|
|
|
|
Future.microtask(() async {
|
|
try {
|
|
await ScreenBrightness().resetScreenBrightness();
|
|
} catch (_) {}
|
|
});
|
|
|
|
_timerSeekBackwardButton?.cancel();
|
|
_timerSeekForwardButton?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
void shiftSubtitle() {
|
|
if (_theme(context).shiftSubtitlesOnControlsVisibilityChange) {
|
|
state(context).setSubtitleViewPadding(
|
|
state(context).widget.subtitleViewConfiguration.padding +
|
|
EdgeInsets.fromLTRB(
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
subtitleVerticalShiftOffset,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
void unshiftSubtitle() {
|
|
if (_theme(context).shiftSubtitlesOnControlsVisibilityChange) {
|
|
state(context).setSubtitleViewPadding(
|
|
state(context).widget.subtitleViewConfiguration.padding,
|
|
);
|
|
}
|
|
}
|
|
|
|
void onTap() {
|
|
if (!visible) {
|
|
setState(() {
|
|
mount = true;
|
|
visible = true;
|
|
});
|
|
shiftSubtitle();
|
|
_timer?.cancel();
|
|
_timer = Timer(_theme(context).controlsHoverDuration, () {
|
|
if (mounted) {
|
|
setState(() {
|
|
visible = false;
|
|
});
|
|
unshiftSubtitle();
|
|
}
|
|
});
|
|
} else {
|
|
setState(() {
|
|
visible = false;
|
|
});
|
|
unshiftSubtitle();
|
|
_timer?.cancel();
|
|
}
|
|
}
|
|
|
|
void onDoubleTapSeekBackward() {
|
|
setState(() {
|
|
_mountSeekBackwardButton = true;
|
|
});
|
|
}
|
|
|
|
void onDoubleTapSeekForward() {
|
|
setState(() {
|
|
_mountSeekForwardButton = true;
|
|
});
|
|
}
|
|
|
|
void onHorizontalDragUpdate(DragUpdateDetails details) {
|
|
if (_dragInitialDelta == Offset.zero) {
|
|
_dragInitialDelta = details.localPosition;
|
|
return;
|
|
}
|
|
|
|
final diff = _dragInitialDelta.dx - details.localPosition.dx;
|
|
final duration = controller(context).player.state.duration.inSeconds;
|
|
final position = controller(context).player.state.position.inSeconds;
|
|
|
|
final seconds = -(diff * duration / _theme(context).horizontalGestureSensitivity).round();
|
|
final relativePosition = position + seconds;
|
|
|
|
if (relativePosition <= duration && relativePosition >= 0) {
|
|
setState(() {
|
|
swipeDuration = seconds;
|
|
showSwipeDuration = true;
|
|
_seekBarDeltaValueNotifier.value = Duration(seconds: seconds);
|
|
});
|
|
}
|
|
}
|
|
|
|
void onHorizontalDragEnd() {
|
|
if (swipeDuration != 0) {
|
|
Duration newPosition = controller(context).player.state.position + Duration(seconds: swipeDuration);
|
|
newPosition = newPosition.clamp(
|
|
Duration.zero,
|
|
controller(context).player.state.duration,
|
|
);
|
|
controller(context).player.seek(newPosition);
|
|
}
|
|
|
|
setState(() {
|
|
_dragInitialDelta = Offset.zero;
|
|
showSwipeDuration = false;
|
|
});
|
|
}
|
|
|
|
bool _isInSegment(double localX, int segmentIndex) {
|
|
List<int> segmentRatios = _theme(context).seekOnDoubleTapLayoutTapsRatios;
|
|
|
|
int totalRatios = segmentRatios.reduce((a, b) => a + b);
|
|
|
|
double segmentWidthMultiplier = widgetWidth(context) / totalRatios;
|
|
double start = 0;
|
|
double end;
|
|
|
|
for (int i = 0; i < segmentRatios.length; i++) {
|
|
end = start + (segmentWidthMultiplier * segmentRatios[i]);
|
|
|
|
if (i == segmentIndex && localX >= start && localX <= end) {
|
|
return true;
|
|
}
|
|
|
|
start = end;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool _isInRightSegment(double localX) {
|
|
return _isInSegment(localX, 2);
|
|
}
|
|
|
|
bool _isInCenterSegment(double localX) {
|
|
return _isInSegment(localX, 1);
|
|
}
|
|
|
|
bool _isInLeftSegment(double localX) {
|
|
return _isInSegment(localX, 0);
|
|
}
|
|
|
|
void _handlePointerDown(PointerDownEvent event) {
|
|
if (!(_isInCenterSegment(event.position.dx))) {
|
|
return;
|
|
}
|
|
|
|
onTap();
|
|
}
|
|
|
|
void _handleTapDown(TapDownDetails details) {
|
|
if ((_isInCenterSegment(details.localPosition.dx))) {
|
|
return;
|
|
}
|
|
|
|
onTap();
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
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;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
var seekOnDoubleTapEnabledWhileControlsAreVisible =
|
|
(_theme(context).seekOnDoubleTap && _theme(context).seekOnDoubleTapEnabledWhileControlsVisible);
|
|
assert(_theme(context).seekOnDoubleTapLayoutTapsRatios.length == 3,
|
|
"The number of seekOnDoubleTapLayoutTapsRatios must be 3, i.e. [1, 1, 1]");
|
|
assert(_theme(context).seekOnDoubleTapLayoutWidgetRatios.length == 3,
|
|
"The number of seekOnDoubleTapLayoutWidgetRatios must be 3, i.e. [1, 1, 1]");
|
|
return Theme(
|
|
data: Theme.of(context).copyWith(
|
|
focusColor: const Color(0x00000000),
|
|
hoverColor: const Color(0x00000000),
|
|
splashColor: const Color(0x00000000),
|
|
highlightColor: const Color(0x00000000),
|
|
),
|
|
child: Focus(
|
|
autofocus: true,
|
|
child: Material(
|
|
elevation: 0.0,
|
|
borderOnForeground: false,
|
|
animationDuration: Duration.zero,
|
|
color: const Color(0x00000000),
|
|
shadowColor: const Color(0x00000000),
|
|
surfaceTintColor: const Color(0x00000000),
|
|
child: Stack(
|
|
clipBehavior: Clip.none,
|
|
alignment: Alignment.center,
|
|
children: [
|
|
AnimatedOpacity(
|
|
curve: Curves.easeInOut,
|
|
opacity: visible ? 1.0 : 0.0,
|
|
duration: _theme(context).controlsTransitionDuration,
|
|
onEnd: () {
|
|
setState(() {
|
|
if (!visible) {
|
|
mount = false;
|
|
}
|
|
});
|
|
},
|
|
child: Stack(
|
|
clipBehavior: Clip.none,
|
|
alignment: Alignment.center,
|
|
children: [
|
|
Positioned.fill(
|
|
child: Container(
|
|
color: _theme(context).backdropColor,
|
|
),
|
|
),
|
|
Positioned.fill(
|
|
left: 16.0,
|
|
top: 16.0,
|
|
right: 16.0,
|
|
bottom: 16.0 + subtitleVerticalShiftOffset,
|
|
child: Listener(
|
|
onPointerDown: (event) => _handlePointerDown(event),
|
|
child: GestureDetector(
|
|
onTapDown: (details) => _handleTapDown(details),
|
|
onDoubleTapDown: _handleDoubleTapDown,
|
|
onLongPress: _theme(context).speedUpOnLongPress ? _handleLongPress : null,
|
|
onLongPressEnd: _theme(context).speedUpOnLongPress ? _handleLongPressEnd : null,
|
|
onDoubleTap: () {
|
|
if (_tapPosition == null) {
|
|
return;
|
|
}
|
|
if (_isInRightSegment(_tapPosition!.dx)) {
|
|
if ((!mount && _theme(context).seekOnDoubleTap) ||
|
|
seekOnDoubleTapEnabledWhileControlsAreVisible) {
|
|
onDoubleTapSeekForward();
|
|
}
|
|
} else {
|
|
if (_isInLeftSegment(_tapPosition!.dx)) {
|
|
if ((!mount && _theme(context).seekOnDoubleTap) ||
|
|
seekOnDoubleTapEnabledWhileControlsAreVisible) {
|
|
onDoubleTapSeekBackward();
|
|
}
|
|
}
|
|
}
|
|
},
|
|
onHorizontalDragUpdate: (details) {
|
|
if ((!mount && _theme(context).seekGesture) ||
|
|
(_theme(context).seekGesture && _theme(context).gesturesEnabledWhileControlsVisible)) {
|
|
onHorizontalDragUpdate(details);
|
|
}
|
|
},
|
|
onHorizontalDragEnd: (details) {
|
|
onHorizontalDragEnd();
|
|
},
|
|
child: Container(
|
|
color: const Color(0x00000000),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if (mount)
|
|
Padding(
|
|
padding: _theme(context).padding ??
|
|
(isFullscreen(context) ? MediaQuery.of(context).padding : EdgeInsets.zero),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
Container(
|
|
height: _theme(context).buttonBarHeight,
|
|
margin: _theme(context).topButtonBarMargin,
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.max,
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: _theme(context).topButtonBar,
|
|
),
|
|
),
|
|
Expanded(
|
|
child: AnimatedOpacity(
|
|
curve: Curves.easeInOut,
|
|
opacity: buffering ? 0.0 : 1.0,
|
|
duration: _theme(context).controlsTransitionDuration,
|
|
child: Center(
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: _theme(context).primaryButtonBar,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Stack(
|
|
alignment: Alignment.bottomCenter,
|
|
children: [
|
|
if (_theme(context).displaySeekBar)
|
|
MaterialSeekBar(
|
|
onSeekStart: () {
|
|
_timer?.cancel();
|
|
},
|
|
onSeekEnd: () {
|
|
_timer = Timer(
|
|
_theme(context).controlsHoverDuration,
|
|
() {
|
|
if (mounted) {
|
|
setState(() {
|
|
visible = false;
|
|
});
|
|
unshiftSubtitle();
|
|
}
|
|
},
|
|
);
|
|
},
|
|
),
|
|
Container(
|
|
height: _theme(context).buttonBarHeight,
|
|
margin: _theme(context).bottomButtonBarMargin,
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.max,
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: _theme(context).bottomButtonBar,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (!mount)
|
|
if (_mountSeekBackwardButton || _mountSeekForwardButton || showSwipeDuration)
|
|
Column(
|
|
children: [
|
|
const Spacer(),
|
|
Stack(
|
|
alignment: Alignment.bottomCenter,
|
|
children: [
|
|
if (_theme(context).displaySeekBar)
|
|
MaterialSeekBar(
|
|
delta: _seekBarDeltaValueNotifier,
|
|
),
|
|
Container(
|
|
height: _theme(context).buttonBarHeight,
|
|
margin: _theme(context).bottomButtonBarMargin,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
IgnorePointer(
|
|
child: Padding(
|
|
padding: _theme(context).padding ??
|
|
(isFullscreen(context) ? MediaQuery.of(context).padding : EdgeInsets.zero),
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
height: _theme(context).buttonBarHeight,
|
|
margin: _theme(context).topButtonBarMargin,
|
|
),
|
|
Expanded(
|
|
child: Center(
|
|
child: TweenAnimationBuilder<double>(
|
|
tween: Tween<double>(
|
|
begin: 0.0,
|
|
end: buffering ? 1.0 : 0.0,
|
|
),
|
|
duration: _theme(context).controlsTransitionDuration,
|
|
builder: (context, value, child) {
|
|
if (value > 0.0) {
|
|
return Opacity(
|
|
opacity: value,
|
|
child: _theme(context).bufferingIndicatorBuilder?.call(context) ?? child!,
|
|
);
|
|
}
|
|
return const SizedBox.shrink();
|
|
},
|
|
child: const CircularProgressIndicator(
|
|
color: Color(0xFFFFFFFF),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
height: _theme(context).buttonBarHeight,
|
|
margin: _theme(context).bottomButtonBarMargin,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
IgnorePointer(
|
|
child: AnimatedOpacity(
|
|
curve: Curves.easeInOut,
|
|
opacity:
|
|
(!mount || _theme(context).gesturesEnabledWhileControlsVisible) && _volumeIndicator ? 1.0 : 0.0,
|
|
duration: _theme(context).controlsTransitionDuration,
|
|
child: _theme(context).volumeIndicatorBuilder?.call(context, _volumeValue) ??
|
|
Container(
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0x88000000),
|
|
borderRadius: BorderRadius.circular(64.0),
|
|
),
|
|
height: 52.0,
|
|
width: 108.0,
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
height: 52.0,
|
|
width: 42.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: 24.0,
|
|
),
|
|
),
|
|
const SizedBox(width: 8.0),
|
|
Expanded(
|
|
child: Text(
|
|
'${(_volumeValue * 100.0).round()}%',
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(
|
|
fontSize: 14.0,
|
|
color: Color(0xFFFFFFFF),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 16.0),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
IgnorePointer(
|
|
child: AnimatedOpacity(
|
|
curve: Curves.easeInOut,
|
|
opacity: (!mount || _theme(context).gesturesEnabledWhileControlsVisible) && _brightnessIndicator
|
|
? 1.0
|
|
: 0.0,
|
|
duration: _theme(context).controlsTransitionDuration,
|
|
child: _theme(context).brightnessIndicatorBuilder?.call(context, _volumeValue) ??
|
|
Container(
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0x88000000),
|
|
borderRadius: BorderRadius.circular(64.0),
|
|
),
|
|
height: 52.0,
|
|
width: 108.0,
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
height: 52.0,
|
|
width: 42.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: 24.0,
|
|
),
|
|
),
|
|
const SizedBox(width: 8.0),
|
|
Expanded(
|
|
child: Text(
|
|
'${(_brightnessValue * 100.0).round()}%',
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(
|
|
fontSize: 14.0,
|
|
color: Color(0xFFFFFFFF),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 16.0),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
IgnorePointer(
|
|
child: Padding(
|
|
padding: _theme(context).padding ??
|
|
(isFullscreen(context) ? MediaQuery.of(context).padding : EdgeInsets.zero),
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
height: _theme(context).buttonBarHeight,
|
|
margin: _theme(context).topButtonBarMargin,
|
|
),
|
|
Expanded(
|
|
child: AnimatedOpacity(
|
|
duration: _theme(context).controlsTransitionDuration,
|
|
opacity: _speedUpIndicator ? 1 : 0,
|
|
child:
|
|
_theme(context).speedUpIndicatorBuilder?.call(context, _theme(context).speedUpFactor) ??
|
|
Container(
|
|
alignment: Alignment.topCenter,
|
|
child: Container(
|
|
margin: const EdgeInsets.all(16.0),
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0x88000000),
|
|
borderRadius: BorderRadius.circular(64.0),
|
|
),
|
|
height: 48.0,
|
|
width: 108.0,
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
const SizedBox(width: 16.0),
|
|
Expanded(
|
|
child: Text(
|
|
'${_theme(context).speedUpFactor.toStringAsFixed(1)}x',
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(
|
|
fontSize: 14.0,
|
|
color: Color(0xFFFFFFFF),
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
height: 48.0,
|
|
width: 48.0 - 16.0,
|
|
alignment: Alignment.centerRight,
|
|
child: const Icon(
|
|
Icons.fast_forward,
|
|
color: Color(0xFFFFFFFF),
|
|
size: 24.0,
|
|
),
|
|
),
|
|
const SizedBox(width: 16.0),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
height: _theme(context).buttonBarHeight,
|
|
margin: _theme(context).bottomButtonBarMargin,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
IgnorePointer(
|
|
child: AnimatedOpacity(
|
|
duration: _theme(context).controlsTransitionDuration,
|
|
opacity: showSwipeDuration ? 1 : 0,
|
|
child: _theme(context).seekIndicatorBuilder?.call(context, Duration(seconds: swipeDuration)) ??
|
|
Container(
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0x88000000),
|
|
borderRadius: BorderRadius.circular(64.0),
|
|
),
|
|
height: 52.0,
|
|
width: 108.0,
|
|
child: Text(
|
|
swipeDuration > 0
|
|
? "+ ${Duration(seconds: swipeDuration).label()}"
|
|
: "- ${Duration(seconds: swipeDuration).label()}",
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(
|
|
fontSize: 14.0,
|
|
color: Color(0xFFFFFFFF),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if (!mount || seekOnDoubleTapEnabledWhileControlsAreVisible)
|
|
if (_mountSeekBackwardButton || _mountSeekForwardButton)
|
|
Positioned.fill(
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
flex: _theme(context).seekOnDoubleTapLayoutWidgetRatios[0],
|
|
child: _mountSeekBackwardButton
|
|
? AnimatedOpacity(
|
|
opacity: _hideSeekBackwardButton ? 0 : 1.0,
|
|
duration: const Duration(milliseconds: 200),
|
|
child: _BackwardSeekIndicator(
|
|
onChanged: (value) {
|
|
_seekBarDeltaValueNotifier.value = -value;
|
|
},
|
|
onSubmitted: (value) {
|
|
_timerSeekBackwardButton?.cancel();
|
|
_timerSeekBackwardButton = Timer(
|
|
const Duration(milliseconds: 200),
|
|
() {
|
|
setState(() {
|
|
_hideSeekBackwardButton = false;
|
|
_mountSeekBackwardButton = false;
|
|
});
|
|
},
|
|
);
|
|
|
|
setState(() {
|
|
_hideSeekBackwardButton = true;
|
|
});
|
|
var result = controller(context).player.state.position - value;
|
|
result = result.clamp(
|
|
Duration.zero,
|
|
controller(context).player.state.duration,
|
|
);
|
|
controller(context).player.seek(result);
|
|
},
|
|
),
|
|
)
|
|
: const SizedBox(),
|
|
),
|
|
if (_theme(context).seekOnDoubleTapLayoutWidgetRatios[1] > 0)
|
|
Expanded(flex: _theme(context).seekOnDoubleTapLayoutWidgetRatios[1], child: SizedBox()),
|
|
Expanded(
|
|
flex: _theme(context).seekOnDoubleTapLayoutWidgetRatios[2],
|
|
child: _mountSeekForwardButton
|
|
? AnimatedOpacity(
|
|
opacity: _hideSeekForwardButton ? 0 : 1.0,
|
|
duration: const Duration(milliseconds: 200),
|
|
child: _ForwardSeekIndicator(
|
|
onChanged: (value) {
|
|
_seekBarDeltaValueNotifier.value = value;
|
|
},
|
|
onSubmitted: (value) {
|
|
_timerSeekForwardButton?.cancel();
|
|
_timerSeekForwardButton = Timer(const Duration(milliseconds: 200), () {
|
|
if (_hideSeekForwardButton) {
|
|
setState(() {
|
|
_hideSeekForwardButton = false;
|
|
_mountSeekForwardButton = false;
|
|
});
|
|
}
|
|
});
|
|
setState(() {
|
|
_hideSeekForwardButton = true;
|
|
});
|
|
|
|
var result = controller(context).player.state.position + value;
|
|
result = result.clamp(
|
|
Duration.zero,
|
|
controller(context).player.state.duration,
|
|
);
|
|
controller(context).player.seek(result);
|
|
},
|
|
),
|
|
)
|
|
: const SizedBox(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
double widgetWidth(BuildContext context) => (context.findRenderObject() as RenderBox).paintBounds.width;
|
|
}
|
|
|
|
class _BackwardSeekIndicator extends StatefulWidget {
|
|
final void Function(Duration) onChanged;
|
|
final void Function(Duration) onSubmitted;
|
|
const _BackwardSeekIndicator({
|
|
Key? key,
|
|
required this.onChanged,
|
|
required this.onSubmitted,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<_BackwardSeekIndicator> createState() => _BackwardSeekIndicatorState();
|
|
}
|
|
|
|
class _BackwardSeekIndicatorState extends State<_BackwardSeekIndicator> {
|
|
Duration value = const Duration(seconds: 10);
|
|
|
|
Timer? timer;
|
|
|
|
@override
|
|
void setState(VoidCallback fn) {
|
|
if (mounted) {
|
|
super.setState(fn);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
timer = Timer(const Duration(milliseconds: 400), () {
|
|
widget.onSubmitted.call(value);
|
|
});
|
|
}
|
|
|
|
void increment() {
|
|
timer?.cancel();
|
|
timer = Timer(const Duration(milliseconds: 400), () {
|
|
widget.onSubmitted.call(value);
|
|
});
|
|
widget.onChanged.call(value);
|
|
setState(() {
|
|
value += const Duration(seconds: 10);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
decoration: const BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
Color(0x88767676),
|
|
Color(0x00767676),
|
|
],
|
|
begin: Alignment.centerLeft,
|
|
end: Alignment.centerRight,
|
|
),
|
|
),
|
|
child: InkWell(
|
|
splashColor: const Color(0x44767676),
|
|
onTap: increment,
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
const Icon(
|
|
Icons.fast_rewind,
|
|
size: 24.0,
|
|
color: Color(0xFFFFFFFF),
|
|
),
|
|
const SizedBox(height: 8.0),
|
|
Text(
|
|
'${value.inSeconds} seconds',
|
|
style: const TextStyle(
|
|
fontSize: 12.0,
|
|
color: Color(0xFFFFFFFF),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ForwardSeekIndicator extends StatefulWidget {
|
|
final void Function(Duration) onChanged;
|
|
final void Function(Duration) onSubmitted;
|
|
const _ForwardSeekIndicator({
|
|
Key? key,
|
|
required this.onChanged,
|
|
required this.onSubmitted,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<_ForwardSeekIndicator> createState() => _ForwardSeekIndicatorState();
|
|
}
|
|
|
|
class _ForwardSeekIndicatorState extends State<_ForwardSeekIndicator> {
|
|
Duration value = const Duration(seconds: 10);
|
|
|
|
Timer? timer;
|
|
|
|
@override
|
|
void setState(VoidCallback fn) {
|
|
if (mounted) {
|
|
super.setState(fn);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
timer = Timer(const Duration(milliseconds: 400), () {
|
|
widget.onSubmitted.call(value);
|
|
});
|
|
}
|
|
|
|
void increment() {
|
|
timer?.cancel();
|
|
timer = Timer(const Duration(milliseconds: 400), () {
|
|
widget.onSubmitted.call(value);
|
|
});
|
|
widget.onChanged.call(value);
|
|
setState(() {
|
|
value += const Duration(seconds: 10);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
decoration: const BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
Color(0x00767676),
|
|
Color(0x88767676),
|
|
],
|
|
begin: Alignment.centerLeft,
|
|
end: Alignment.centerRight,
|
|
),
|
|
),
|
|
child: InkWell(
|
|
splashColor: const Color(0x44767676),
|
|
onTap: increment,
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
const Icon(
|
|
Icons.fast_forward,
|
|
size: 24.0,
|
|
color: Color(0xFFFFFFFF),
|
|
),
|
|
const SizedBox(height: 8.0),
|
|
Text(
|
|
'${value.inSeconds} seconds',
|
|
style: const TextStyle(
|
|
fontSize: 12.0,
|
|
color: Color(0xFFFFFFFF),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|