2063 lines
75 KiB
Dart
2063 lines
75 KiB
Dart
![]() |
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||
|
///
|
||
|
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||
|
/// All rights reserved.
|
||
|
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||
|
library;
|
||
|
|
||
|
// ignore_for_file: non_constant_identifier_names
|
||
|
import 'dart:async';
|
||
|
|
||
|
import 'package:flutter/material.dart';
|
||
|
import 'package:media_kit_video/media_kit_video.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/methods/video_state.dart';
|
||
|
import 'package:media_kit_video/media_kit_video_controls/src/controls/widgets/video_controls_theme_data_injector.dart';
|
||
|
import 'package:screen_brightness_platform_interface/screen_brightness_platform_interface.dart';
|
||
|
import 'package:volume_controller/volume_controller.dart';
|
||
|
|
||
|
/// {@template material_video_controls}
|
||
|
///
|
||
|
/// [Video] controls which use Material design.
|
||
|
///
|
||
|
/// {@endtemplate}
|
||
|
Widget MyMaterialVideoControls(VideoState state) {
|
||
|
return const VideoControlsThemeDataInjector(
|
||
|
child: _MaterialVideoControls(),
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/// [MaterialVideoControlsThemeData] available in this [context].
|
||
|
MaterialVideoControlsThemeData _theme(BuildContext context) => FullscreenInheritedWidget.maybeOf(context) == null
|
||
|
? MaterialVideoControlsTheme.maybeOf(context)?.normal ?? kDefaultMaterialVideoControlsThemeData
|
||
|
: MaterialVideoControlsTheme.maybeOf(context)?.fullscreen ?? kDefaultMaterialVideoControlsThemeDataFullscreen;
|
||
|
|
||
|
/// Default [MaterialVideoControlsThemeData].
|
||
|
const kDefaultMaterialVideoControlsThemeData = MaterialVideoControlsThemeData();
|
||
|
|
||
|
/// Default [MaterialVideoControlsThemeData] for fullscreen.
|
||
|
const kDefaultMaterialVideoControlsThemeDataFullscreen = MaterialVideoControlsThemeData(
|
||
|
displaySeekBar: true,
|
||
|
automaticallyImplySkipNextButton: true,
|
||
|
automaticallyImplySkipPreviousButton: true,
|
||
|
volumeGesture: true,
|
||
|
brightnessGesture: true,
|
||
|
seekGesture: true,
|
||
|
gesturesEnabledWhileControlsVisible: true,
|
||
|
seekOnDoubleTap: true,
|
||
|
seekOnDoubleTapEnabledWhileControlsVisible: true,
|
||
|
visibleOnMount: false,
|
||
|
speedUpOnLongPress: false,
|
||
|
speedUpFactor: 2.0,
|
||
|
verticalGestureSensitivity: 100,
|
||
|
horizontalGestureSensitivity: 1000,
|
||
|
backdropColor: Color(0x66000000),
|
||
|
padding: null,
|
||
|
controlsHoverDuration: Duration(seconds: 3),
|
||
|
controlsTransitionDuration: Duration(milliseconds: 300),
|
||
|
bufferingIndicatorBuilder: null,
|
||
|
volumeIndicatorBuilder: null,
|
||
|
brightnessIndicatorBuilder: null,
|
||
|
seekIndicatorBuilder: null,
|
||
|
speedUpIndicatorBuilder: null,
|
||
|
primaryButtonBar: [
|
||
|
Spacer(flex: 2),
|
||
|
MaterialSkipPreviousButton(),
|
||
|
Spacer(),
|
||
|
MaterialPlayOrPauseButton(iconSize: 56.0),
|
||
|
Spacer(),
|
||
|
MaterialSkipNextButton(),
|
||
|
Spacer(flex: 2),
|
||
|
],
|
||
|
topButtonBar: [],
|
||
|
topButtonBarMargin: EdgeInsets.symmetric(
|
||
|
horizontal: 16.0,
|
||
|
),
|
||
|
bottomButtonBar: [
|
||
|
MaterialPositionIndicator(),
|
||
|
Spacer(),
|
||
|
// 全屏后的
|
||
|
MaterialFullscreenButton(),
|
||
|
],
|
||
|
bottomButtonBarMargin: EdgeInsets.only(
|
||
|
left: 16.0,
|
||
|
right: 8.0,
|
||
|
bottom: 42.0,
|
||
|
),
|
||
|
buttonBarHeight: 56.0,
|
||
|
buttonBarButtonSize: 24.0,
|
||
|
buttonBarButtonColor: Color(0xFFFFFFFF),
|
||
|
seekBarMargin: EdgeInsets.only(
|
||
|
left: 16.0,
|
||
|
right: 16.0,
|
||
|
bottom: 42.0,
|
||
|
),
|
||
|
seekBarHeight: 2.4,
|
||
|
seekBarContainerHeight: 36.0,
|
||
|
seekBarColor: Color(0x3DFFFFFF),
|
||
|
seekBarPositionColor: Color(0xFFFF0000),
|
||
|
seekBarBufferColor: Color(0x3DFFFFFF),
|
||
|
seekBarThumbSize: 12.8,
|
||
|
seekBarThumbColor: Color(0xFFFF0000),
|
||
|
seekBarAlignment: Alignment.bottomCenter,
|
||
|
shiftSubtitlesOnControlsVisibilityChange: false,
|
||
|
);
|
||
|
|
||
|
/// {@template material_video_controls_theme_data}
|
||
|
///
|
||
|
/// Theming related data for [MaterialVideoControls]. These values are used to theme the descendant [MaterialVideoControls].
|
||
|
///
|
||
|
/// {@endtemplate}
|
||
|
class MaterialVideoControlsThemeData {
|
||
|
// BEHAVIOR
|
||
|
|
||
|
/// Whether to display seek bar.
|
||
|
final bool displaySeekBar;
|
||
|
|
||
|
/// Whether a skip next button should be displayed if there are more than one videos in the playlist.
|
||
|
final bool automaticallyImplySkipNextButton;
|
||
|
|
||
|
/// Whether a skip previous button should be displayed if there are more than one videos in the playlist.
|
||
|
final bool automaticallyImplySkipPreviousButton;
|
||
|
|
||
|
/// Whether to modify volume on vertical drag gesture on the right side of the screen.
|
||
|
final bool volumeGesture;
|
||
|
|
||
|
/// Whether to modify screen brightness on vertical drag gesture on the left side of the screen.
|
||
|
final bool brightnessGesture;
|
||
|
|
||
|
/// Whether to seek on horizontal drag gesture.
|
||
|
final bool seekGesture;
|
||
|
|
||
|
/// Whether to allow gesture controls to work while controls are visible.
|
||
|
/// NOTE: This option is ignored when gestures are false.
|
||
|
final bool gesturesEnabledWhileControlsVisible;
|
||
|
|
||
|
/// Whether to enable double tap to seek on left or right side of the screen.
|
||
|
final bool seekOnDoubleTap;
|
||
|
|
||
|
/// Whether to allow double tap to seek on left or right side of the screen to work while controls are visible.
|
||
|
/// NOTE: This option is ignored when [seekOnDoubleTap] is false.
|
||
|
final bool seekOnDoubleTapEnabledWhileControlsVisible;
|
||
|
|
||
|
/// `seekOnDoubleTapLayoutTapsRatios` defines the width proportions for the interactive areas
|
||
|
/// responsible for seek actions (backward seek, instant tap, forward seek) when a double tap
|
||
|
/// occurs on the video widget. This property divides the video widget into three segments
|
||
|
/// horizontally. Each integer in the list represents the relative width of each segment.
|
||
|
/// By default, the value `[1, 1, 1]` means that the video widget is equally divided into three
|
||
|
/// segments: the left segment for backward seek, the middle segment for instant tap (usually show and hide controls),
|
||
|
/// and the right segment for forward seek. Adjusting these values changes the width of the interactive areas
|
||
|
/// for each double tap action.
|
||
|
final List<int> seekOnDoubleTapLayoutTapsRatios;
|
||
|
|
||
|
/// `seekOnDoubleTapLayoutWidgetRatios` defines the width proportions for the visual indicators or
|
||
|
/// widgets that appear during the double tap actions (backward seek, instant tap, forward seek).
|
||
|
/// Similar to `seekOnDoubleTapLayoutTapsRatios`, it divides the area where these indicators are
|
||
|
/// displayed into three segments. Each integer in the list represents the relative width of each
|
||
|
/// segment where the corresponding indicators will be shown. The default `[1, 1, 1]` equally divides
|
||
|
/// the space for each indicator. Modifying these values can change the layout of the seek indicators,
|
||
|
/// giving more or less space to each one based on the specified ratios.
|
||
|
final List<int> seekOnDoubleTapLayoutWidgetRatios;
|
||
|
|
||
|
/// Duration of seek on double tap backward.
|
||
|
final Duration seekOnDoubleTapBackwardDuration;
|
||
|
|
||
|
/// Duration of seek on double tap forward.
|
||
|
final Duration seekOnDoubleTapForwardDuration;
|
||
|
|
||
|
/// Whether the controls are initially visible.
|
||
|
final bool visibleOnMount;
|
||
|
|
||
|
/// Whether to speed up on long press.
|
||
|
final bool speedUpOnLongPress;
|
||
|
|
||
|
/// Factor to speed up on long press.
|
||
|
final double speedUpFactor;
|
||
|
|
||
|
/// Gesture sensitivity on vertical drag gestures, the higher the value is the less sensitive the gesture.
|
||
|
final double verticalGestureSensitivity;
|
||
|
|
||
|
/// Gesture sensitivity on horizontal drag gestures, the higher the value is the less sensitive the gesture.
|
||
|
final double horizontalGestureSensitivity;
|
||
|
|
||
|
/// Color of backdrop that comes up when controls are visible.
|
||
|
final Color? backdropColor;
|
||
|
|
||
|
// GENERIC
|
||
|
|
||
|
/// Padding around the controls.
|
||
|
///
|
||
|
/// * Default: `EdgeInsets.zero`
|
||
|
/// * FullScreen: `MediaQuery.of(context).padding`
|
||
|
///
|
||
|
/// NOTE: In fullscreen, this will be safe area (set [padding] to [EdgeInsets.zero] to disable safe area)
|
||
|
final EdgeInsets? padding;
|
||
|
|
||
|
/// [Duration] after which the controls will be hidden when there is no mouse movement.
|
||
|
final Duration controlsHoverDuration;
|
||
|
|
||
|
/// [Duration] for which the controls will be animated when shown or hidden.
|
||
|
final Duration controlsTransitionDuration;
|
||
|
|
||
|
/// Builder for the buffering indicator.
|
||
|
final Widget Function(BuildContext)? bufferingIndicatorBuilder;
|
||
|
|
||
|
/// Custom builder for volume indicator.
|
||
|
final Widget Function(BuildContext, double)? volumeIndicatorBuilder;
|
||
|
|
||
|
/// Custom builder for brightness indicator.
|
||
|
final Widget Function(BuildContext, double)? brightnessIndicatorBuilder;
|
||
|
|
||
|
/// Custom builder for seek indicator.
|
||
|
final Widget Function(BuildContext, Duration)? seekIndicatorBuilder;
|
||
|
|
||
|
/// Custom builder for seek indicator.
|
||
|
final Widget Function(BuildContext, double)? speedUpIndicatorBuilder;
|
||
|
|
||
|
// BUTTON BAR
|
||
|
|
||
|
/// Buttons to be displayed in the primary button bar.
|
||
|
final List<Widget> primaryButtonBar;
|
||
|
|
||
|
/// Buttons to be displayed in the top button bar.
|
||
|
final List<Widget> topButtonBar;
|
||
|
|
||
|
/// Margin around the top button bar.
|
||
|
final EdgeInsets topButtonBarMargin;
|
||
|
|
||
|
/// Buttons to be displayed in the bottom button bar.
|
||
|
final List<Widget> bottomButtonBar;
|
||
|
|
||
|
/// Margin around the button bar.
|
||
|
final EdgeInsets bottomButtonBarMargin;
|
||
|
|
||
|
/// Height of the button bar.
|
||
|
final double buttonBarHeight;
|
||
|
|
||
|
/// Size of the button bar buttons.
|
||
|
final double buttonBarButtonSize;
|
||
|
|
||
|
/// Color of the button bar buttons.
|
||
|
final Color buttonBarButtonColor;
|
||
|
|
||
|
// SEEK BAR
|
||
|
|
||
|
/// Margin around the seek bar.
|
||
|
final EdgeInsets seekBarMargin;
|
||
|
|
||
|
/// Height of the seek bar.
|
||
|
final double seekBarHeight;
|
||
|
|
||
|
/// Height of the seek bar [Container].
|
||
|
final double seekBarContainerHeight;
|
||
|
|
||
|
/// [Color] of the seek bar.
|
||
|
final Color seekBarColor;
|
||
|
|
||
|
/// [Color] of the playback position section in the seek bar.
|
||
|
final Color seekBarPositionColor;
|
||
|
|
||
|
/// [Color] of the playback buffer section in the seek bar.
|
||
|
final Color seekBarBufferColor;
|
||
|
|
||
|
/// Size of the seek bar thumb.
|
||
|
final double seekBarThumbSize;
|
||
|
|
||
|
/// [Color] of the seek bar thumb.
|
||
|
final Color seekBarThumbColor;
|
||
|
|
||
|
/// [Alignment] of seek bar inside the seek bar container.
|
||
|
final Alignment seekBarAlignment;
|
||
|
|
||
|
// SUBTITLE
|
||
|
|
||
|
/// Whether to shift the subtitles upwards when the controls are visible.
|
||
|
final bool shiftSubtitlesOnControlsVisibilityChange;
|
||
|
|
||
|
/// {@macro material_video_controls_theme_data}
|
||
|
const MaterialVideoControlsThemeData({
|
||
|
this.displaySeekBar = true,
|
||
|
this.automaticallyImplySkipNextButton = true,
|
||
|
this.automaticallyImplySkipPreviousButton = true,
|
||
|
this.volumeGesture = false,
|
||
|
this.brightnessGesture = false,
|
||
|
this.seekGesture = false,
|
||
|
this.gesturesEnabledWhileControlsVisible = true,
|
||
|
this.seekOnDoubleTap = false,
|
||
|
this.seekOnDoubleTapEnabledWhileControlsVisible = true,
|
||
|
this.seekOnDoubleTapLayoutTapsRatios = const [1, 1, 1],
|
||
|
this.seekOnDoubleTapLayoutWidgetRatios = const [1, 1, 1],
|
||
|
this.seekOnDoubleTapBackwardDuration = const Duration(seconds: 10),
|
||
|
this.seekOnDoubleTapForwardDuration = const Duration(seconds: 10),
|
||
|
this.visibleOnMount = false,
|
||
|
this.speedUpOnLongPress = false,
|
||
|
this.speedUpFactor = 2.0,
|
||
|
this.verticalGestureSensitivity = 100,
|
||
|
this.horizontalGestureSensitivity = 1000,
|
||
|
this.backdropColor = const Color(0x66000000),
|
||
|
this.padding,
|
||
|
this.controlsHoverDuration = const Duration(seconds: 3),
|
||
|
this.controlsTransitionDuration = const Duration(milliseconds: 300),
|
||
|
this.bufferingIndicatorBuilder,
|
||
|
this.volumeIndicatorBuilder,
|
||
|
this.brightnessIndicatorBuilder,
|
||
|
this.seekIndicatorBuilder,
|
||
|
this.speedUpIndicatorBuilder,
|
||
|
this.primaryButtonBar = const [
|
||
|
Spacer(flex: 2),
|
||
|
MaterialSkipPreviousButton(),
|
||
|
Spacer(),
|
||
|
MaterialPlayOrPauseButton(iconSize: 48.0),
|
||
|
Spacer(),
|
||
|
MaterialSkipNextButton(),
|
||
|
Spacer(flex: 2),
|
||
|
],
|
||
|
this.topButtonBar = const [],
|
||
|
this.topButtonBarMargin = const EdgeInsets.symmetric(horizontal: 16.0),
|
||
|
this.bottomButtonBar = const [
|
||
|
MaterialPositionIndicator(),
|
||
|
Spacer(),
|
||
|
// 未全屏的
|
||
|
// MaterialFullscreenButton(),
|
||
|
],
|
||
|
this.bottomButtonBarMargin = const EdgeInsets.only(left: 16.0, right: 8.0),
|
||
|
this.buttonBarHeight = 56.0,
|
||
|
this.buttonBarButtonSize = 24.0,
|
||
|
this.buttonBarButtonColor = const Color(0xFFFFFFFF),
|
||
|
this.seekBarMargin = EdgeInsets.zero,
|
||
|
this.seekBarHeight = 2.4,
|
||
|
this.seekBarContainerHeight = 36.0,
|
||
|
this.seekBarColor = const Color(0x3DFFFFFF),
|
||
|
this.seekBarPositionColor = const Color(0xFFFF0000),
|
||
|
this.seekBarBufferColor = const Color(0x3DFFFFFF),
|
||
|
this.seekBarThumbSize = 12.8,
|
||
|
this.seekBarThumbColor = const Color(0xFFFF0000),
|
||
|
this.seekBarAlignment = Alignment.bottomCenter,
|
||
|
this.shiftSubtitlesOnControlsVisibilityChange = false,
|
||
|
});
|
||
|
|
||
|
/// Creates a copy of this [MaterialVideoControlsThemeData] with the given fields replaced by the non-null parameter values.
|
||
|
MaterialVideoControlsThemeData copyWith({
|
||
|
bool? displaySeekBar,
|
||
|
bool? automaticallyImplySkipNextButton,
|
||
|
bool? automaticallyImplySkipPreviousButton,
|
||
|
bool? volumeGesture,
|
||
|
bool? brightnessGesture,
|
||
|
bool? seekGesture,
|
||
|
bool? gesturesEnabledWhileControlsVisible,
|
||
|
bool? seekOnDoubleTap,
|
||
|
bool? seekOnDoubleTapEnabledWhileControlsVisible,
|
||
|
List<int>? seekOnDoubleTapLayoutTapsRatios,
|
||
|
List<int>? seekOnDoubleTapLayoutWidgetRatios,
|
||
|
Duration? seekOnDoubleTapBackwardDuration,
|
||
|
Duration? seekOnDoubleTapForwardDuration,
|
||
|
bool? visibleOnMount,
|
||
|
bool? speedUpOnLongPress,
|
||
|
double? speedUpFactor,
|
||
|
double? verticalGestureSensitivity,
|
||
|
double? horizontalGestureSensitivity,
|
||
|
Color? backdropColor,
|
||
|
Duration? controlsHoverDuration,
|
||
|
Duration? controlsTransitionDuration,
|
||
|
Widget Function(BuildContext)? bufferingIndicatorBuilder,
|
||
|
Widget Function(BuildContext, double)? volumeIndicatorBuilder,
|
||
|
Widget Function(BuildContext, double)? brightnessIndicatorBuilder,
|
||
|
Widget Function(BuildContext, Duration)? seekIndicatorBuilder,
|
||
|
Widget Function(BuildContext, double)? speedUpIndicatorBuilder,
|
||
|
List<Widget>? primaryButtonBar,
|
||
|
List<Widget>? topButtonBar,
|
||
|
EdgeInsets? topButtonBarMargin,
|
||
|
List<Widget>? bottomButtonBar,
|
||
|
EdgeInsets? bottomButtonBarMargin,
|
||
|
double? buttonBarHeight,
|
||
|
double? buttonBarButtonSize,
|
||
|
Color? buttonBarButtonColor,
|
||
|
EdgeInsets? seekBarMargin,
|
||
|
double? seekBarHeight,
|
||
|
double? seekBarContainerHeight,
|
||
|
Color? seekBarColor,
|
||
|
Color? seekBarPositionColor,
|
||
|
Color? seekBarBufferColor,
|
||
|
double? seekBarThumbSize,
|
||
|
Color? seekBarThumbColor,
|
||
|
Alignment? seekBarAlignment,
|
||
|
bool? shiftSubtitlesOnControlsVisibilityChange,
|
||
|
}) {
|
||
|
return MaterialVideoControlsThemeData(
|
||
|
displaySeekBar: displaySeekBar ?? this.displaySeekBar,
|
||
|
automaticallyImplySkipNextButton: automaticallyImplySkipNextButton ?? this.automaticallyImplySkipNextButton,
|
||
|
automaticallyImplySkipPreviousButton: automaticallyImplySkipPreviousButton ?? this.automaticallyImplySkipPreviousButton,
|
||
|
volumeGesture: volumeGesture ?? this.volumeGesture,
|
||
|
brightnessGesture: brightnessGesture ?? this.brightnessGesture,
|
||
|
seekGesture: seekGesture ?? this.seekGesture,
|
||
|
gesturesEnabledWhileControlsVisible: gesturesEnabledWhileControlsVisible ?? this.gesturesEnabledWhileControlsVisible,
|
||
|
seekOnDoubleTap: seekOnDoubleTap ?? this.seekOnDoubleTap,
|
||
|
seekOnDoubleTapEnabledWhileControlsVisible: seekOnDoubleTapEnabledWhileControlsVisible ?? this.seekOnDoubleTapEnabledWhileControlsVisible,
|
||
|
seekOnDoubleTapLayoutTapsRatios: seekOnDoubleTapLayoutTapsRatios ?? this.seekOnDoubleTapLayoutTapsRatios,
|
||
|
seekOnDoubleTapLayoutWidgetRatios: seekOnDoubleTapLayoutWidgetRatios ?? this.seekOnDoubleTapLayoutWidgetRatios,
|
||
|
seekOnDoubleTapBackwardDuration: seekOnDoubleTapBackwardDuration ?? this.seekOnDoubleTapBackwardDuration,
|
||
|
seekOnDoubleTapForwardDuration: seekOnDoubleTapForwardDuration ?? this.seekOnDoubleTapForwardDuration,
|
||
|
visibleOnMount: visibleOnMount ?? this.visibleOnMount,
|
||
|
speedUpOnLongPress: speedUpOnLongPress ?? this.speedUpOnLongPress,
|
||
|
speedUpFactor: speedUpFactor ?? this.speedUpFactor,
|
||
|
verticalGestureSensitivity: verticalGestureSensitivity ?? this.verticalGestureSensitivity,
|
||
|
horizontalGestureSensitivity: horizontalGestureSensitivity ?? this.horizontalGestureSensitivity,
|
||
|
backdropColor: backdropColor ?? this.backdropColor,
|
||
|
controlsHoverDuration: controlsHoverDuration ?? this.controlsHoverDuration,
|
||
|
controlsTransitionDuration: controlsTransitionDuration ?? this.controlsTransitionDuration,
|
||
|
bufferingIndicatorBuilder: bufferingIndicatorBuilder ?? this.bufferingIndicatorBuilder,
|
||
|
volumeIndicatorBuilder: volumeIndicatorBuilder ?? this.volumeIndicatorBuilder,
|
||
|
brightnessIndicatorBuilder: brightnessIndicatorBuilder ?? this.brightnessIndicatorBuilder,
|
||
|
seekIndicatorBuilder: seekIndicatorBuilder ?? this.seekIndicatorBuilder,
|
||
|
speedUpIndicatorBuilder: speedUpIndicatorBuilder ?? this.speedUpIndicatorBuilder,
|
||
|
primaryButtonBar: primaryButtonBar ?? this.primaryButtonBar,
|
||
|
topButtonBar: topButtonBar ?? this.topButtonBar,
|
||
|
topButtonBarMargin: topButtonBarMargin ?? this.topButtonBarMargin,
|
||
|
bottomButtonBar: bottomButtonBar ?? this.bottomButtonBar,
|
||
|
bottomButtonBarMargin: bottomButtonBarMargin ?? this.bottomButtonBarMargin,
|
||
|
buttonBarHeight: buttonBarHeight ?? this.buttonBarHeight,
|
||
|
buttonBarButtonSize: buttonBarButtonSize ?? this.buttonBarButtonSize,
|
||
|
buttonBarButtonColor: buttonBarButtonColor ?? this.buttonBarButtonColor,
|
||
|
seekBarMargin: seekBarMargin ?? this.seekBarMargin,
|
||
|
seekBarHeight: seekBarHeight ?? this.seekBarHeight,
|
||
|
seekBarContainerHeight: seekBarContainerHeight ?? this.seekBarContainerHeight,
|
||
|
seekBarColor: seekBarColor ?? this.seekBarColor,
|
||
|
seekBarPositionColor: seekBarPositionColor ?? this.seekBarPositionColor,
|
||
|
seekBarBufferColor: seekBarBufferColor ?? this.seekBarBufferColor,
|
||
|
seekBarThumbSize: seekBarThumbSize ?? this.seekBarThumbSize,
|
||
|
seekBarThumbColor: seekBarThumbColor ?? this.seekBarThumbColor,
|
||
|
seekBarAlignment: seekBarAlignment ?? this.seekBarAlignment,
|
||
|
shiftSubtitlesOnControlsVisibilityChange: shiftSubtitlesOnControlsVisibilityChange ?? this.shiftSubtitlesOnControlsVisibilityChange,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// {@template material_video_controls_theme}
|
||
|
///
|
||
|
/// Inherited widget which provides [MaterialVideoControlsThemeData] to descendant widgets.
|
||
|
///
|
||
|
/// {@endtemplate}
|
||
|
class MaterialVideoControlsTheme extends InheritedWidget {
|
||
|
final MaterialVideoControlsThemeData normal;
|
||
|
final MaterialVideoControlsThemeData fullscreen;
|
||
|
const MaterialVideoControlsTheme({
|
||
|
super.key,
|
||
|
required this.normal,
|
||
|
required this.fullscreen,
|
||
|
required super.child,
|
||
|
});
|
||
|
|
||
|
static MaterialVideoControlsTheme? maybeOf(BuildContext context) {
|
||
|
return context.dependOnInheritedWidgetOfExactType<MaterialVideoControlsTheme>();
|
||
|
}
|
||
|
|
||
|
static MaterialVideoControlsTheme of(BuildContext context) {
|
||
|
final MaterialVideoControlsTheme? result = maybeOf(context);
|
||
|
assert(
|
||
|
result != null,
|
||
|
'No [MaterialVideoControlsTheme] found in [context]',
|
||
|
);
|
||
|
return result!;
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
bool updateShouldNotify(MaterialVideoControlsTheme oldWidget) => identical(normal, oldWidget.normal) && identical(fullscreen, oldWidget.fullscreen);
|
||
|
}
|
||
|
|
||
|
/// {@macro material_video_controls}
|
||
|
class _MaterialVideoControls extends StatefulWidget {
|
||
|
const _MaterialVideoControls();
|
||
|
|
||
|
@override
|
||
|
State<_MaterialVideoControls> createState() => _MaterialVideoControlsState();
|
||
|
}
|
||
|
|
||
|
/// {@macro material_video_controls}
|
||
|
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 _currentRate = 1.0;
|
||
|
double _volumeValue = 0.0;
|
||
|
bool _volumeIndicator = false;
|
||
|
Timer? _volumeTimer;
|
||
|
// The default event stream in package:volume_controller is buggy.
|
||
|
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;
|
||
|
final VolumeController _volumeController = VolumeController.instance;
|
||
|
|
||
|
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;
|
||
|
});
|
||
|
_currentRate = controller(context).player.state.rate;
|
||
|
controller(context).player.setRate(_theme(context).speedUpFactor);
|
||
|
}
|
||
|
|
||
|
void _handleLongPressEnd(LongPressEndDetails details) {
|
||
|
setState(() {
|
||
|
_speedUpIndicator = false;
|
||
|
});
|
||
|
controller(context).player.setRate(_currentRate);
|
||
|
}
|
||
|
|
||
|
@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();
|
||
|
}
|
||
|
// --------------------------------------------------
|
||
|
// package:screen_brightness
|
||
|
Future.microtask(() async {
|
||
|
try {
|
||
|
await ScreenBrightnessPlatform.instance.resetApplicationScreenBrightness();
|
||
|
} 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) {
|
||
|
// Local variable with the list of ratios
|
||
|
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]);
|
||
|
|
||
|
// Check if the current index matches the segmentIndex and if localX falls within it
|
||
|
if (i == segmentIndex && localX >= start && localX <= end) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// Set the start of the next segment
|
||
|
start = end;
|
||
|
}
|
||
|
|
||
|
// If localX does not fall within the specified segment
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
bool _isInRightSegment(double localX) {
|
||
|
return _isInSegment(localX, 2);
|
||
|
}
|
||
|
|
||
|
bool _isInLeftSegment(double localX) {
|
||
|
return _isInSegment(localX, 0);
|
||
|
}
|
||
|
|
||
|
void _handlePointerDown(PointerDownEvent event) {
|
||
|
onTap();
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
void initState() {
|
||
|
super.initState();
|
||
|
// --------------------------------------------------
|
||
|
// package:volume_controller
|
||
|
Future.microtask(() async {
|
||
|
try {
|
||
|
_volumeController.showSystemUI = false;
|
||
|
_volumeValue = await _volumeController.getVolume();
|
||
|
_volumeController.addListener((value) {
|
||
|
if (mounted && !_volumeInterceptEventStream) {
|
||
|
setState(() {
|
||
|
_volumeValue = value;
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
} catch (_) {}
|
||
|
});
|
||
|
// --------------------------------------------------
|
||
|
// --------------------------------------------------
|
||
|
// package:screen_brightness
|
||
|
Future.microtask(() async {
|
||
|
try {
|
||
|
_brightnessValue = await ScreenBrightnessPlatform.instance.application;
|
||
|
ScreenBrightnessPlatform.instance.onApplicationScreenBrightnessChanged.listen((value) {
|
||
|
if (mounted) {
|
||
|
setState(() {
|
||
|
_brightnessValue = value;
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
} catch (_) {}
|
||
|
});
|
||
|
// --------------------------------------------------
|
||
|
}
|
||
|
|
||
|
Future<void> setVolume(double value) async {
|
||
|
// --------------------------------------------------
|
||
|
// package:volume_controller
|
||
|
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 {
|
||
|
// --------------------------------------------------
|
||
|
// package:screen_brightness
|
||
|
try {
|
||
|
await ScreenBrightnessPlatform.instance.setApplicationScreenBrightness(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: [
|
||
|
// Controls:
|
||
|
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,
|
||
|
),
|
||
|
),
|
||
|
// We are adding 16.0 boundary around the actual controls (which contain the vertical drag gesture detectors).
|
||
|
// This will make the hit-test on edges (e.g. swiping to: show status-bar, show navigation-bar, go back in navigation) not activate the swipe gesture annoyingly.
|
||
|
Positioned.fill(
|
||
|
left: 16.0,
|
||
|
top: 16.0,
|
||
|
right: 16.0,
|
||
|
bottom: 16.0 + subtitleVerticalShiftOffset,
|
||
|
child: Listener(
|
||
|
onPointerDown: (event) => _handlePointerDown(event),
|
||
|
child: GestureDetector(
|
||
|
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();
|
||
|
},
|
||
|
onVerticalDragUpdate: (e) async {
|
||
|
final delta = e.delta.dy;
|
||
|
final Offset position = e.localPosition;
|
||
|
|
||
|
if (position.dx <= widgetWidth(context) / 2) {
|
||
|
// Left side of screen swiped
|
||
|
if ((!mount && _theme(context).brightnessGesture) ||
|
||
|
(_theme(context).brightnessGesture && _theme(context).gesturesEnabledWhileControlsVisible)) {
|
||
|
final brightness = _brightnessValue - delta / _theme(context).verticalGestureSensitivity;
|
||
|
final result = brightness.clamp(0.0, 1.0);
|
||
|
setBrightness(result);
|
||
|
}
|
||
|
} else {
|
||
|
// Right side of screen swiped
|
||
|
|
||
|
if ((!mount && _theme(context).volumeGesture) ||
|
||
|
(_theme(context).volumeGesture && _theme(context).gesturesEnabledWhileControlsVisible)) {
|
||
|
final volume = _volumeValue - delta / _theme(context).verticalGestureSensitivity;
|
||
|
final result = volume.clamp(0.0, 1.0);
|
||
|
setVolume(result);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
child: Container(
|
||
|
color: const Color(0x00000000),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
if (mount)
|
||
|
Padding(
|
||
|
padding: _theme(context).padding ??
|
||
|
(
|
||
|
// Add padding in fullscreen!
|
||
|
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,
|
||
|
),
|
||
|
),
|
||
|
// Only display [primaryButtonBar] if [buffering] is false.
|
||
|
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,
|
||
|
),
|
||
|
),
|
||
|
],
|
||
|
),
|
||
|
],
|
||
|
),
|
||
|
),
|
||
|
],
|
||
|
),
|
||
|
),
|
||
|
// Double-Tap Seek Seek-Bar:
|
||
|
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,
|
||
|
),
|
||
|
],
|
||
|
),
|
||
|
],
|
||
|
),
|
||
|
// Buffering Indicator.
|
||
|
IgnorePointer(
|
||
|
child: Padding(
|
||
|
padding: _theme(context).padding ??
|
||
|
(
|
||
|
// Add padding in fullscreen!
|
||
|
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) {
|
||
|
// Only mount the buffering indicator if the opacity is greater than 0.0.
|
||
|
// This has been done to prevent redundant resource usage in [CircularProgressIndicator].
|
||
|
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,
|
||
|
),
|
||
|
],
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
// Volume Indicator.
|
||
|
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),
|
||
|
],
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
// Brightness Indicator.
|
||
|
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, _brightnessValue) ??
|
||
|
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),
|
||
|
],
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
// Speedup Indicator.
|
||
|
IgnorePointer(
|
||
|
child: Padding(
|
||
|
padding: _theme(context).padding ??
|
||
|
(
|
||
|
// Add padding in fullscreen!
|
||
|
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,
|
||
|
),
|
||
|
],
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
// Seek Indicator.
|
||
|
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),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
|
||
|
// Double-Tap Seek Button(s):
|
||
|
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(
|
||
|
duration: _theme(context).seekOnDoubleTapBackwardDuration,
|
||
|
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(),
|
||
|
),
|
||
|
//Area in the middle where the double-tap seek buttons are ignored in
|
||
|
if (_theme(context).seekOnDoubleTapLayoutWidgetRatios[1] > 0)
|
||
|
Spacer(
|
||
|
flex: _theme(context).seekOnDoubleTapLayoutWidgetRatios[1],
|
||
|
),
|
||
|
Expanded(
|
||
|
flex: _theme(context).seekOnDoubleTapLayoutWidgetRatios[2],
|
||
|
child: _mountSeekForwardButton
|
||
|
? AnimatedOpacity(
|
||
|
opacity: _hideSeekForwardButton ? 0 : 1.0,
|
||
|
duration: const Duration(milliseconds: 200),
|
||
|
child: _ForwardSeekIndicator(
|
||
|
duration: _theme(context).seekOnDoubleTapForwardDuration,
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
// SEEK BAR
|
||
|
|
||
|
/// Material design seek bar.
|
||
|
class MaterialSeekBar extends StatefulWidget {
|
||
|
final ValueNotifier<Duration>? delta;
|
||
|
final VoidCallback? onSeekStart;
|
||
|
final VoidCallback? onSeekEnd;
|
||
|
|
||
|
const MaterialSeekBar({
|
||
|
super.key,
|
||
|
this.delta,
|
||
|
this.onSeekStart,
|
||
|
this.onSeekEnd,
|
||
|
});
|
||
|
|
||
|
@override
|
||
|
MaterialSeekBarState createState() => MaterialSeekBarState();
|
||
|
}
|
||
|
|
||
|
class MaterialSeekBarState extends State<MaterialSeekBar> {
|
||
|
bool tapped = false;
|
||
|
double slider = 0.0;
|
||
|
|
||
|
late bool playing = controller(context).player.state.playing;
|
||
|
late Duration position = controller(context).player.state.position;
|
||
|
late Duration duration = controller(context).player.state.duration;
|
||
|
late Duration buffer = controller(context).player.state.buffer;
|
||
|
|
||
|
final List<StreamSubscription> subscriptions = [];
|
||
|
|
||
|
@override
|
||
|
void setState(VoidCallback fn) {
|
||
|
if (mounted) {
|
||
|
super.setState(fn);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void listener() {
|
||
|
setState(() {
|
||
|
final delta = widget.delta?.value ?? Duration.zero;
|
||
|
position = controller(context).player.state.position + delta;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
void initState() {
|
||
|
super.initState();
|
||
|
widget.delta?.addListener(listener);
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
void didChangeDependencies() {
|
||
|
super.didChangeDependencies();
|
||
|
if (subscriptions.isEmpty && widget.delta == null) {
|
||
|
subscriptions.addAll(
|
||
|
[
|
||
|
controller(context).player.stream.playing.listen((event) {
|
||
|
setState(() {
|
||
|
playing = event;
|
||
|
});
|
||
|
}),
|
||
|
controller(context).player.stream.completed.listen((event) {
|
||
|
setState(() {
|
||
|
position = Duration.zero;
|
||
|
});
|
||
|
}),
|
||
|
controller(context).player.stream.position.listen((event) {
|
||
|
setState(() {
|
||
|
if (!tapped) {
|
||
|
position = event;
|
||
|
}
|
||
|
});
|
||
|
}),
|
||
|
controller(context).player.stream.duration.listen((event) {
|
||
|
setState(() {
|
||
|
duration = event;
|
||
|
});
|
||
|
}),
|
||
|
controller(context).player.stream.buffer.listen((event) {
|
||
|
setState(() {
|
||
|
buffer = event;
|
||
|
});
|
||
|
}),
|
||
|
],
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
void dispose() {
|
||
|
widget.delta?.removeListener(listener);
|
||
|
for (final subscription in subscriptions) {
|
||
|
subscription.cancel();
|
||
|
}
|
||
|
super.dispose();
|
||
|
}
|
||
|
|
||
|
void onPointerMove(PointerMoveEvent e, BoxConstraints constraints) {
|
||
|
final percent = e.localPosition.dx / constraints.maxWidth;
|
||
|
setState(() {
|
||
|
tapped = true;
|
||
|
slider = percent.clamp(0.0, 1.0);
|
||
|
});
|
||
|
controller(context).player.seek(duration * slider);
|
||
|
}
|
||
|
|
||
|
void onPointerDown() {
|
||
|
widget.onSeekStart?.call();
|
||
|
setState(() {
|
||
|
tapped = true;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
void onPointerUp() {
|
||
|
widget.onSeekEnd?.call();
|
||
|
setState(() {
|
||
|
// Explicitly set the position to prevent the slider from jumping.
|
||
|
tapped = false;
|
||
|
position = duration * slider;
|
||
|
});
|
||
|
controller(context).player.seek(duration * slider);
|
||
|
}
|
||
|
|
||
|
void onPanStart(DragStartDetails e, BoxConstraints constraints) {
|
||
|
final percent = e.localPosition.dx / constraints.maxWidth;
|
||
|
setState(() {
|
||
|
tapped = true;
|
||
|
slider = percent.clamp(0.0, 1.0);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
void onPanDown(DragDownDetails e, BoxConstraints constraints) {
|
||
|
final percent = e.localPosition.dx / constraints.maxWidth;
|
||
|
setState(() {
|
||
|
tapped = true;
|
||
|
slider = percent.clamp(0.0, 1.0);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
void onPanUpdate(DragUpdateDetails e, BoxConstraints constraints) {
|
||
|
final percent = e.localPosition.dx / constraints.maxWidth;
|
||
|
setState(() {
|
||
|
tapped = true;
|
||
|
slider = percent.clamp(0.0, 1.0);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/// Returns the current playback position in percentage.
|
||
|
double get positionPercent {
|
||
|
if (position == Duration.zero || duration == Duration.zero) {
|
||
|
return 0.0;
|
||
|
} else {
|
||
|
final value = position.inMilliseconds / duration.inMilliseconds;
|
||
|
return value.clamp(0.0, 1.0);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Returns the current playback buffer position in percentage.
|
||
|
double get bufferPercent {
|
||
|
if (buffer == Duration.zero || duration == Duration.zero) {
|
||
|
return 0.0;
|
||
|
} else {
|
||
|
final value = buffer.inMilliseconds / duration.inMilliseconds;
|
||
|
return value.clamp(0.0, 1.0);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
Widget build(BuildContext context) {
|
||
|
return Container(
|
||
|
clipBehavior: Clip.none,
|
||
|
margin: _theme(context).seekBarMargin,
|
||
|
child: LayoutBuilder(
|
||
|
builder: (context, constraints) => MouseRegion(
|
||
|
cursor: SystemMouseCursors.click,
|
||
|
child: GestureDetector(
|
||
|
onHorizontalDragUpdate: (_) {},
|
||
|
onPanStart: (e) => onPanStart(e, constraints),
|
||
|
onPanDown: (e) => onPanDown(e, constraints),
|
||
|
onPanUpdate: (e) => onPanUpdate(e, constraints),
|
||
|
child: Listener(
|
||
|
onPointerMove: (e) => onPointerMove(e, constraints),
|
||
|
onPointerDown: (e) => onPointerDown(),
|
||
|
onPointerUp: (e) => onPointerUp(),
|
||
|
child: Container(
|
||
|
color: Colors.transparent,
|
||
|
width: constraints.maxWidth,
|
||
|
alignment: _theme(context).seekBarAlignment,
|
||
|
height: _theme(context).seekBarContainerHeight,
|
||
|
child: Stack(
|
||
|
clipBehavior: Clip.none,
|
||
|
alignment: Alignment.bottomCenter,
|
||
|
children: [
|
||
|
Container(
|
||
|
width: constraints.maxWidth,
|
||
|
height: _theme(context).seekBarHeight,
|
||
|
alignment: Alignment.bottomLeft,
|
||
|
color: _theme(context).seekBarColor,
|
||
|
child: Stack(
|
||
|
clipBehavior: Clip.none,
|
||
|
alignment: Alignment.bottomLeft,
|
||
|
children: [
|
||
|
Container(
|
||
|
width: constraints.maxWidth * bufferPercent,
|
||
|
color: _theme(context).seekBarBufferColor,
|
||
|
),
|
||
|
Container(
|
||
|
width: tapped ? constraints.maxWidth * slider : constraints.maxWidth * positionPercent,
|
||
|
color: _theme(context).seekBarPositionColor,
|
||
|
),
|
||
|
],
|
||
|
),
|
||
|
),
|
||
|
Positioned(
|
||
|
left: tapped
|
||
|
? (constraints.maxWidth - _theme(context).seekBarThumbSize / 2) * slider
|
||
|
: (constraints.maxWidth - _theme(context).seekBarThumbSize / 2) * positionPercent,
|
||
|
bottom: -1.0 * _theme(context).seekBarThumbSize / 2 + _theme(context).seekBarHeight / 2,
|
||
|
child: Container(
|
||
|
width: _theme(context).seekBarThumbSize,
|
||
|
height: _theme(context).seekBarThumbSize,
|
||
|
decoration: BoxDecoration(
|
||
|
color: _theme(context).seekBarThumbColor,
|
||
|
borderRadius: BorderRadius.circular(
|
||
|
_theme(context).seekBarThumbSize / 2,
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
],
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// BUTTON: PLAY/PAUSE
|
||
|
|
||
|
/// A material design play/pause button.
|
||
|
class MaterialPlayOrPauseButton extends StatefulWidget {
|
||
|
/// Overriden icon size for [MaterialSkipPreviousButton].
|
||
|
final double? iconSize;
|
||
|
|
||
|
/// Overriden icon color for [MaterialSkipPreviousButton].
|
||
|
final Color? iconColor;
|
||
|
|
||
|
const MaterialPlayOrPauseButton({
|
||
|
super.key,
|
||
|
this.iconSize,
|
||
|
this.iconColor,
|
||
|
});
|
||
|
|
||
|
@override
|
||
|
MaterialPlayOrPauseButtonState createState() => MaterialPlayOrPauseButtonState();
|
||
|
}
|
||
|
|
||
|
class MaterialPlayOrPauseButtonState extends State<MaterialPlayOrPauseButton> with SingleTickerProviderStateMixin {
|
||
|
late final animation = AnimationController(
|
||
|
vsync: this,
|
||
|
value: controller(context).player.state.playing ? 1 : 0,
|
||
|
duration: const Duration(milliseconds: 200),
|
||
|
);
|
||
|
|
||
|
StreamSubscription<bool>? subscription;
|
||
|
|
||
|
@override
|
||
|
void setState(VoidCallback fn) {
|
||
|
if (mounted) {
|
||
|
super.setState(fn);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
void didChangeDependencies() {
|
||
|
super.didChangeDependencies();
|
||
|
subscription ??= controller(context).player.stream.playing.listen((event) {
|
||
|
if (event) {
|
||
|
animation.forward();
|
||
|
} else {
|
||
|
animation.reverse();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
void dispose() {
|
||
|
animation.dispose();
|
||
|
subscription?.cancel();
|
||
|
super.dispose();
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
Widget build(BuildContext context) {
|
||
|
return IconButton(
|
||
|
onPressed: controller(context).player.playOrPause,
|
||
|
iconSize: widget.iconSize ?? _theme(context).buttonBarButtonSize,
|
||
|
color: widget.iconColor ?? _theme(context).buttonBarButtonColor,
|
||
|
icon: IgnorePointer(
|
||
|
child: AnimatedIcon(
|
||
|
progress: animation,
|
||
|
icon: AnimatedIcons.play_pause,
|
||
|
size: widget.iconSize ?? _theme(context).buttonBarButtonSize,
|
||
|
color: widget.iconColor ?? _theme(context).buttonBarButtonColor,
|
||
|
),
|
||
|
),
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// BUTTON: SKIP NEXT
|
||
|
|
||
|
/// Material design skip next button.
|
||
|
class MaterialSkipNextButton extends StatelessWidget {
|
||
|
/// Icon for [MaterialSkipNextButton].
|
||
|
final Widget? icon;
|
||
|
|
||
|
/// Overriden icon size for [MaterialSkipNextButton].
|
||
|
final double? iconSize;
|
||
|
|
||
|
/// Overriden icon color for [MaterialSkipNextButton].
|
||
|
final Color? iconColor;
|
||
|
|
||
|
const MaterialSkipNextButton({
|
||
|
super.key,
|
||
|
this.icon,
|
||
|
this.iconSize,
|
||
|
this.iconColor,
|
||
|
});
|
||
|
|
||
|
@override
|
||
|
Widget build(BuildContext context) {
|
||
|
if (!_theme(context).automaticallyImplySkipNextButton ||
|
||
|
(controller(context).player.state.playlist.medias.length > 1 && _theme(context).automaticallyImplySkipNextButton)) {
|
||
|
return IconButton(
|
||
|
onPressed: controller(context).player.next,
|
||
|
icon: icon ?? const Icon(Icons.skip_next),
|
||
|
iconSize: iconSize ?? _theme(context).buttonBarButtonSize,
|
||
|
color: iconColor ?? _theme(context).buttonBarButtonColor,
|
||
|
);
|
||
|
}
|
||
|
return const SizedBox.shrink();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// BUTTON: SKIP PREVIOUS
|
||
|
|
||
|
/// Material design skip previous button.
|
||
|
class MaterialSkipPreviousButton extends StatelessWidget {
|
||
|
/// Icon for [MaterialSkipPreviousButton].
|
||
|
final Widget? icon;
|
||
|
|
||
|
/// Overriden icon size for [MaterialSkipPreviousButton].
|
||
|
final double? iconSize;
|
||
|
|
||
|
/// Overriden icon color for [MaterialSkipPreviousButton].
|
||
|
final Color? iconColor;
|
||
|
|
||
|
const MaterialSkipPreviousButton({
|
||
|
super.key,
|
||
|
this.icon,
|
||
|
this.iconSize,
|
||
|
this.iconColor,
|
||
|
});
|
||
|
|
||
|
@override
|
||
|
Widget build(BuildContext context) {
|
||
|
if (!_theme(context).automaticallyImplySkipPreviousButton ||
|
||
|
(controller(context).player.state.playlist.medias.length > 1 && _theme(context).automaticallyImplySkipPreviousButton)) {
|
||
|
return IconButton(
|
||
|
onPressed: controller(context).player.previous,
|
||
|
icon: icon ?? const Icon(Icons.skip_previous),
|
||
|
iconSize: iconSize ?? _theme(context).buttonBarButtonSize,
|
||
|
color: iconColor ?? _theme(context).buttonBarButtonColor,
|
||
|
);
|
||
|
}
|
||
|
return const SizedBox.shrink();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// BUTTON: FULL SCREEN
|
||
|
|
||
|
/// Material design fullscreen button.
|
||
|
class MaterialFullscreenButton extends StatelessWidget {
|
||
|
/// Icon for [MaterialFullscreenButton].
|
||
|
final Widget? icon;
|
||
|
|
||
|
/// Overriden icon size for [MaterialFullscreenButton].
|
||
|
final double? iconSize;
|
||
|
|
||
|
/// Overriden icon color for [MaterialFullscreenButton].
|
||
|
final Color? iconColor;
|
||
|
|
||
|
const MaterialFullscreenButton({
|
||
|
super.key,
|
||
|
this.icon,
|
||
|
this.iconSize,
|
||
|
this.iconColor,
|
||
|
});
|
||
|
|
||
|
@override
|
||
|
Widget build(BuildContext context) {
|
||
|
return IconButton(
|
||
|
onPressed: () => toggleFullscreen(context),
|
||
|
icon: icon ?? (isFullscreen(context) ? const Icon(Icons.fullscreen_exit) : const Icon(Icons.fullscreen)),
|
||
|
iconSize: iconSize ?? _theme(context).buttonBarButtonSize,
|
||
|
color: iconColor ?? _theme(context).buttonBarButtonColor,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// BUTTON: CUSTOM
|
||
|
|
||
|
/// Material design custom button.
|
||
|
class MaterialCustomButton extends StatelessWidget {
|
||
|
/// Icon for [MaterialCustomButton].
|
||
|
final Widget? icon;
|
||
|
|
||
|
/// Icon size for [MaterialCustomButton].
|
||
|
final double? iconSize;
|
||
|
|
||
|
/// Icon color for [MaterialCustomButton].
|
||
|
final Color? iconColor;
|
||
|
|
||
|
/// The callback that is called when the button is tapped or otherwise activated.
|
||
|
final VoidCallback onPressed;
|
||
|
|
||
|
const MaterialCustomButton({
|
||
|
super.key,
|
||
|
this.icon,
|
||
|
this.iconSize,
|
||
|
this.iconColor,
|
||
|
required this.onPressed,
|
||
|
});
|
||
|
|
||
|
@override
|
||
|
Widget build(BuildContext context) {
|
||
|
return IconButton(
|
||
|
onPressed: onPressed,
|
||
|
icon: icon ?? const Icon(Icons.settings),
|
||
|
padding: EdgeInsets.zero,
|
||
|
iconSize: iconSize ?? _theme(context).buttonBarButtonSize,
|
||
|
color: iconColor ?? _theme(context).buttonBarButtonColor,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// POSITION INDICATOR
|
||
|
|
||
|
/// Material design position indicator.
|
||
|
class MaterialPositionIndicator extends StatefulWidget {
|
||
|
/// Overriden [TextStyle] for the [MaterialPositionIndicator].
|
||
|
final TextStyle? style;
|
||
|
const MaterialPositionIndicator({super.key, this.style});
|
||
|
|
||
|
@override
|
||
|
MaterialPositionIndicatorState createState() => MaterialPositionIndicatorState();
|
||
|
}
|
||
|
|
||
|
class MaterialPositionIndicatorState extends State<MaterialPositionIndicator> {
|
||
|
late Duration position = controller(context).player.state.position;
|
||
|
late Duration duration = controller(context).player.state.duration;
|
||
|
|
||
|
final List<StreamSubscription> subscriptions = [];
|
||
|
|
||
|
@override
|
||
|
void setState(VoidCallback fn) {
|
||
|
if (mounted) {
|
||
|
super.setState(fn);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
void didChangeDependencies() {
|
||
|
super.didChangeDependencies();
|
||
|
if (subscriptions.isEmpty) {
|
||
|
subscriptions.addAll(
|
||
|
[
|
||
|
controller(context).player.stream.position.listen((event) {
|
||
|
setState(() {
|
||
|
position = event;
|
||
|
});
|
||
|
}),
|
||
|
controller(context).player.stream.duration.listen((event) {
|
||
|
setState(() {
|
||
|
duration = event;
|
||
|
});
|
||
|
}),
|
||
|
],
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
void dispose() {
|
||
|
for (final subscription in subscriptions) {
|
||
|
subscription.cancel();
|
||
|
}
|
||
|
super.dispose();
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
Widget build(BuildContext context) {
|
||
|
return Text(
|
||
|
'${position.label(reference: duration)} / ${duration.label(reference: duration)}',
|
||
|
style: widget.style ??
|
||
|
TextStyle(
|
||
|
height: 1.0,
|
||
|
fontSize: 12.0,
|
||
|
color: _theme(context).buttonBarButtonColor,
|
||
|
),
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class _BackwardSeekIndicator extends StatefulWidget {
|
||
|
final Duration duration;
|
||
|
final void Function(Duration) onChanged;
|
||
|
final void Function(Duration) onSubmitted;
|
||
|
const _BackwardSeekIndicator({
|
||
|
super.key,
|
||
|
required this.duration,
|
||
|
required this.onChanged,
|
||
|
required this.onSubmitted,
|
||
|
});
|
||
|
|
||
|
@override
|
||
|
State<_BackwardSeekIndicator> createState() => _BackwardSeekIndicatorState();
|
||
|
}
|
||
|
|
||
|
class _BackwardSeekIndicatorState extends State<_BackwardSeekIndicator> {
|
||
|
late Duration value = widget.duration;
|
||
|
|
||
|
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 Duration duration;
|
||
|
final void Function(Duration) onChanged;
|
||
|
final void Function(Duration) onSubmitted;
|
||
|
const _ForwardSeekIndicator({
|
||
|
super.key,
|
||
|
required this.duration,
|
||
|
required this.onChanged,
|
||
|
required this.onSubmitted,
|
||
|
});
|
||
|
|
||
|
@override
|
||
|
State<_ForwardSeekIndicator> createState() => _ForwardSeekIndicatorState();
|
||
|
}
|
||
|
|
||
|
class _ForwardSeekIndicatorState extends State<_ForwardSeekIndicator> {
|
||
|
late Duration value = widget.duration;
|
||
|
|
||
|
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),
|
||
|
),
|
||
|
),
|
||
|
],
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
);
|
||
|
}
|
||
|
}
|