From ea3de04f1cd73e6ca401b37c3585756d0b5f931c Mon Sep 17 00:00:00 2001 From: Lukas Nagel Date: Sun, 3 Nov 2024 17:50:59 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=9A=20Switch=20to=20package=20for=20ma?= =?UTF-8?q?terial=5Ftv?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/views/player.dart | 2 +- lib/widgets/player_controls/material_tv.dart | 1525 ------------------ pubspec.lock | 10 +- pubspec.yaml | 1 + 4 files changed, 11 insertions(+), 1527 deletions(-) delete mode 100644 lib/widgets/player_controls/material_tv.dart diff --git a/lib/views/player.dart b/lib/views/player.dart index bb9a842..a250c19 100644 --- a/lib/views/player.dart +++ b/lib/views/player.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:media_kit/media_kit.dart'; +import 'package:media_kit_tv/material_tv.dart'; import 'package:media_kit_video/media_kit_video.dart'; import 'package:open_media_server_app/apis/base_api.dart'; import 'package:open_media_server_app/globals/platform_globals.dart'; -import 'package:open_media_server_app/widgets/player_controls/material_tv.dart'; import 'package:open_media_server_app/helpers/wrapper.dart'; class PlayerView extends StatefulWidget { diff --git a/lib/widgets/player_controls/material_tv.dart b/lib/widgets/player_controls/material_tv.dart deleted file mode 100644 index 0aa2762..0000000 --- a/lib/widgets/player_controls/material_tv.dart +++ /dev/null @@ -1,1525 +0,0 @@ -/// This file is a part of media_kit (https://github.com/media-kit/media-kit). -/// -/// Copyright © 2021 & onwards, Hitesh Kumar Saini . -/// All rights reserved. -/// Use of this source code is governed by MIT license that can be found in the LICENSE file. -// ignore_for_file: non_constant_identifier_names -import 'dart:async'; -import 'package:flutter/material.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/services.dart'; -import 'package:media_kit_video/media_kit_video.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'; - -/// {@template material_desktop_video_controls} -/// -/// [Video] controls which use Material design. -/// -/// {@endtemplate} -Widget MaterialTvVideoControls(VideoState state) { - return const VideoControlsThemeDataInjector( - child: _MaterialTvVideoControls(), - ); -} - -/// [MaterialTvVideoControlsThemeData] available in this [context]. -MaterialTvVideoControlsThemeData _theme(BuildContext context) => - FullscreenInheritedWidget.maybeOf(context) == null - ? MaterialTvVideoControlsTheme.maybeOf(context)?.normal ?? - kDefaultMaterialDesktopVideoControlsThemeData - : MaterialTvVideoControlsTheme.maybeOf(context)?.fullscreen ?? - kDefaultMaterialDesktopVideoControlsThemeDataFullscreen; - -/// Default [MaterialTvVideoControlsThemeData]. -const kDefaultMaterialDesktopVideoControlsThemeData = - MaterialTvVideoControlsThemeData(); - -/// Default [MaterialTvVideoControlsThemeData] for fullscreen. -const kDefaultMaterialDesktopVideoControlsThemeDataFullscreen = - MaterialTvVideoControlsThemeData(); - -/// {@template material_desktop_video_controls_theme_data} -/// -/// Theming related data for [MaterialTvVideoControls]. These values are used to theme the descendant [MaterialTvVideoControls]. -/// -/// {@endtemplate} -class MaterialTvVideoControlsThemeData { - // 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; - - /// Modify volume on mouse scroll. - final bool modifyVolumeOnScroll; - - /// Whether to toggle fullscreen on double press. - final bool toggleFullscreenOnDoublePress; - - /// Whether to hide mouse on controls removal.(will need to move the mouse to be hidden check issue: https://github.com/flutter/flutter/issues/76622) works on macos without moving the mouse - final bool hideMouseOnControlsRemoval; - - /// Whether to toggle play and pause on tap. - final bool playAndPauseOnTap; - - /// Keyboards shortcuts. - final Map? keyboardShortcuts; - - /// Whether the controls are initially visible. - final bool visibleOnMount; - - // GENERIC - - /// Padding around the controls. - /// - /// * Default: `EdgeInsets.zero` - /// * Fullscreen: `MediaQuery.of(context).padding` - 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; - - // BUTTON BAR - - /// Buttons to be displayed in the primary button bar. - final List primaryButtonBar; - - /// Buttons to be displayed in the top button bar. - final List topButtonBar; - - /// Margin around the top button bar. - final EdgeInsets topButtonBarMargin; - - /// Buttons to be displayed in the bottom button bar. - final List bottomButtonBar; - - /// Margin around the bottom 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 - - /// [Duration] for which the seek bar will be animated when the user seeks. - final Duration seekBarTransitionDuration; - - /// [Duration] for which the seek bar thumb will be animated when the user seeks. - final Duration seekBarThumbTransitionDuration; - - /// Margin around the seek bar. - final EdgeInsets seekBarMargin; - - /// Height of the seek bar. - final double seekBarHeight; - - /// Height of the seek bar when hovered. - final double seekBarHoverHeight; - - /// Height of the seek bar [Container]. - final double seekBarContainerHeight; - - /// [Color] of the seek bar. - final Color seekBarColor; - - /// [Color] of the hovered section in the seek bar. - final Color seekBarHoverColor; - - /// [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; - - // VOLUME BAR - - /// [Color] of the volume bar. - final Color volumeBarColor; - - /// [Color] of the active region in the volume bar. - final Color volumeBarActiveColor; - - /// Size of the volume bar thumb. - final double volumeBarThumbSize; - - /// [Color] of the volume bar thumb. - final Color volumeBarThumbColor; - - /// [Duration] for which the volume bar will be animated when the user hovers. - final Duration volumeBarTransitionDuration; - - // SUBTITLE - - /// Whether to shift the subtitles upwards when the controls are visible. - final bool shiftSubtitlesOnControlsVisibilityChange; - - /// {@macro material_desktop_video_controls_theme_data} - const MaterialTvVideoControlsThemeData({ - this.displaySeekBar = true, - this.automaticallyImplySkipNextButton = true, - this.automaticallyImplySkipPreviousButton = true, - this.toggleFullscreenOnDoublePress = true, - this.playAndPauseOnTap = false, - this.modifyVolumeOnScroll = true, - this.keyboardShortcuts, - this.visibleOnMount = false, - this.hideMouseOnControlsRemoval = false, - this.padding, - this.controlsHoverDuration = const Duration(seconds: 3), - this.controlsTransitionDuration = const Duration(milliseconds: 150), - this.bufferingIndicatorBuilder, - this.primaryButtonBar = const [], - this.topButtonBar = const [], - this.topButtonBarMargin = const EdgeInsets.symmetric(horizontal: 16.0), - this.bottomButtonBar = const [ - MaterialTvSkipPreviousButton(), - MaterialTvPlayOrPauseButton(), - MaterialTvSkipNextButton(), - MaterialTvVolumeButton(), - MaterialTvPositionIndicator(), - Spacer(), - MaterialTvFullscreenButton(), - ], - this.bottomButtonBarMargin = const EdgeInsets.symmetric(horizontal: 16.0), - this.buttonBarHeight = 56.0, - this.buttonBarButtonSize = 28.0, - this.buttonBarButtonColor = const Color(0xFFFFFFFF), - this.seekBarTransitionDuration = const Duration(milliseconds: 300), - this.seekBarThumbTransitionDuration = const Duration(milliseconds: 150), - this.seekBarMargin = const EdgeInsets.symmetric(horizontal: 16.0), - this.seekBarHeight = 3.2, - this.seekBarHoverHeight = 5.6, - this.seekBarContainerHeight = 36.0, - this.seekBarColor = const Color(0x3DFFFFFF), - this.seekBarHoverColor = const Color(0x3DFFFFFF), - this.seekBarPositionColor = const Color(0xFFFF0000), - this.seekBarBufferColor = const Color(0x3DFFFFFF), - this.seekBarThumbSize = 12.0, - this.seekBarThumbColor = const Color(0xFFFF0000), - this.volumeBarColor = const Color(0x3DFFFFFF), - this.volumeBarActiveColor = const Color(0xFFFFFFFF), - this.volumeBarThumbSize = 12.0, - this.volumeBarThumbColor = const Color(0xFFFFFFFF), - this.volumeBarTransitionDuration = const Duration(milliseconds: 150), - this.shiftSubtitlesOnControlsVisibilityChange = true, - }); - - /// Creates a copy of this [MaterialTvVideoControlsThemeData] with the given fields replaced by the non-null parameter values. - MaterialTvVideoControlsThemeData copyWith({ - bool? displaySeekBar, - bool? automaticallyImplySkipNextButton, - bool? automaticallyImplySkipPreviousButton, - bool? toggleFullscreenOnDoublePress, - bool? playAndPauseOnTap, - bool? modifyVolumeOnScroll, - Map? keyboardShortcuts, - bool? visibleOnMount, - bool? hideMouseOnControlsRemoval, - Duration? controlsHoverDuration, - Duration? controlsTransitionDuration, - Widget Function(BuildContext)? bufferingIndicatorBuilder, - List? topButtonBar, - EdgeInsets? topButtonBarMargin, - List? bottomButtonBar, - EdgeInsets? bottomButtonBarMargin, - double? buttonBarHeight, - double? buttonBarButtonSize, - Color? buttonBarButtonColor, - Duration? seekBarTransitionDuration, - Duration? seekBarThumbTransitionDuration, - EdgeInsets? seekBarMargin, - double? seekBarHeight, - double? seekBarHoverHeight, - double? seekBarContainerHeight, - Color? seekBarColor, - Color? seekBarHoverColor, - Color? seekBarPositionColor, - Color? seekBarBufferColor, - double? seekBarThumbSize, - Color? seekBarThumbColor, - Color? volumeBarColor, - Color? volumeBarActiveColor, - double? volumeBarThumbSize, - Color? volumeBarThumbColor, - Duration? volumeBarTransitionDuration, - bool? shiftSubtitlesOnControlsVisibilityChange, - }) { - return MaterialTvVideoControlsThemeData( - displaySeekBar: displaySeekBar ?? this.displaySeekBar, - automaticallyImplySkipNextButton: automaticallyImplySkipNextButton ?? - this.automaticallyImplySkipNextButton, - automaticallyImplySkipPreviousButton: - automaticallyImplySkipPreviousButton ?? - this.automaticallyImplySkipPreviousButton, - toggleFullscreenOnDoublePress: - toggleFullscreenOnDoublePress ?? this.toggleFullscreenOnDoublePress, - playAndPauseOnTap: playAndPauseOnTap ?? this.playAndPauseOnTap, - modifyVolumeOnScroll: modifyVolumeOnScroll ?? this.modifyVolumeOnScroll, - keyboardShortcuts: keyboardShortcuts ?? this.keyboardShortcuts, - visibleOnMount: visibleOnMount ?? this.visibleOnMount, - hideMouseOnControlsRemoval: - hideMouseOnControlsRemoval ?? this.hideMouseOnControlsRemoval, - controlsHoverDuration: - controlsHoverDuration ?? this.controlsHoverDuration, - bufferingIndicatorBuilder: - bufferingIndicatorBuilder ?? this.bufferingIndicatorBuilder, - controlsTransitionDuration: - controlsTransitionDuration ?? this.controlsTransitionDuration, - 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, - seekBarTransitionDuration: - seekBarTransitionDuration ?? this.seekBarTransitionDuration, - seekBarThumbTransitionDuration: - seekBarThumbTransitionDuration ?? this.seekBarThumbTransitionDuration, - seekBarMargin: seekBarMargin ?? this.seekBarMargin, - seekBarHeight: seekBarHeight ?? this.seekBarHeight, - seekBarHoverHeight: seekBarHoverHeight ?? this.seekBarHoverHeight, - seekBarContainerHeight: - seekBarContainerHeight ?? this.seekBarContainerHeight, - seekBarColor: seekBarColor ?? this.seekBarColor, - seekBarHoverColor: seekBarHoverColor ?? this.seekBarHoverColor, - seekBarPositionColor: seekBarPositionColor ?? this.seekBarPositionColor, - seekBarBufferColor: seekBarBufferColor ?? this.seekBarBufferColor, - seekBarThumbSize: seekBarThumbSize ?? this.seekBarThumbSize, - seekBarThumbColor: seekBarThumbColor ?? this.seekBarThumbColor, - volumeBarColor: volumeBarColor ?? this.volumeBarColor, - volumeBarActiveColor: volumeBarActiveColor ?? this.volumeBarActiveColor, - volumeBarThumbSize: volumeBarThumbSize ?? this.volumeBarThumbSize, - volumeBarThumbColor: volumeBarThumbColor ?? this.volumeBarThumbColor, - volumeBarTransitionDuration: - volumeBarTransitionDuration ?? this.volumeBarTransitionDuration, - shiftSubtitlesOnControlsVisibilityChange: - shiftSubtitlesOnControlsVisibilityChange ?? - this.shiftSubtitlesOnControlsVisibilityChange, - ); - } -} - -/// {@template material_desktop_video_controls_theme} -/// -/// Inherited widget which provides [MaterialTvVideoControlsThemeData] to descendant widgets. -/// -/// {@endtemplate} -class MaterialTvVideoControlsTheme extends InheritedWidget { - final MaterialTvVideoControlsThemeData normal; - final MaterialTvVideoControlsThemeData fullscreen; - const MaterialTvVideoControlsTheme({ - super.key, - required this.normal, - required this.fullscreen, - required super.child, - }); - - static MaterialTvVideoControlsTheme? maybeOf(BuildContext context) { - return context - .dependOnInheritedWidgetOfExactType(); - } - - static MaterialTvVideoControlsTheme of(BuildContext context) { - final MaterialTvVideoControlsTheme? result = maybeOf(context); - assert( - result != null, - 'No [MaterialDesktopVideoControlsTheme] found in [context]', - ); - return result!; - } - - @override - bool updateShouldNotify(MaterialTvVideoControlsTheme oldWidget) => - identical(normal, oldWidget.normal) && - identical(fullscreen, oldWidget.fullscreen); -} - -/// {@macro material_desktop_video_controls} -class _MaterialTvVideoControls extends StatefulWidget { - const _MaterialTvVideoControls(); - - @override - State<_MaterialTvVideoControls> createState() => - _MaterialTvVideoControlsState(); -} - -/// {@macro material_desktop_video_controls} -class _MaterialTvVideoControlsState extends State<_MaterialTvVideoControls> { - late bool mount = _theme(context).visibleOnMount; - late bool visible = _theme(context).visibleOnMount; - - Timer? _timer; - - late /* private */ var playlist = controller(context).player.state.playlist; - late bool buffering = controller(context).player.state.buffering; - - DateTime last = DateTime.now(); - - final List subscriptions = []; - - FocusNode _focusNode = FocusNode(); - - double get subtitleVerticalShiftOffset => - (_theme(context).padding?.bottom ?? 0.0) + - (_theme(context).bottomButtonBarMargin.vertical) + - (_theme(context).bottomButtonBar.isNotEmpty - ? _theme(context).buttonBarHeight - : 0.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(); - } - 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 onHover() { - setState(() { - mount = true; - visible = true; - }); - shiftSubtitle(); - _timer?.cancel(); - _timer = Timer(_theme(context).controlsHoverDuration, () { - if (mounted) { - setState(() { - visible = false; - }); - unshiftSubtitle(); - } - }); - } - - void onEnter() { - setState(() { - mount = true; - visible = true; - }); - shiftSubtitle(); - _timer?.cancel(); - _timer = Timer(_theme(context).controlsHoverDuration, () { - if (mounted) { - setState(() { - visible = false; - }); - unshiftSubtitle(); - } - }); - } - - void onExit() { - setState(() { - visible = false; - }); - unshiftSubtitle(); - _timer?.cancel(); - } - - @override - Widget build(BuildContext context) { - return FocusScope( - autofocus: true, - onKeyEvent: (node, event) { - onEnter(); - - if (event is KeyDownEvent) { - print('Key pressed: ${event.logicalKey.debugName}'); - - if (event.logicalKey == LogicalKeyboardKey.mediaPlayPause) { - controller(context).player.playOrPause(); - } - } - - return KeyEventResult.ignored; - }, - child: Theme( - data: Theme.of(context).copyWith( - focusColor: const Color(0x00000000), - hoverColor: const Color(0x00000000), - splashColor: const Color(0x00000000), - highlightColor: const Color(0x00000000), - ), - child: Material( - elevation: 0.0, - borderOnForeground: false, - animationDuration: Duration.zero, - color: const Color(0x00000000), - shadowColor: const Color(0x00000000), - surfaceTintColor: const Color(0x00000000), - child: Stack( - children: [ - AnimatedOpacity( - curve: Curves.easeInOut, - opacity: visible ? 1.0 : 0.0, - duration: _theme(context).controlsTransitionDuration, - onEnd: () { - if (!visible) { - setState(() { - mount = false; - }); - } - }, - child: Stack( - clipBehavior: Clip.none, - alignment: Alignment.bottomCenter, - children: [ - // Top gradient. - if (_theme(context).topButtonBar.isNotEmpty) - Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - stops: [ - 0.0, - 0.2, - ], - colors: [ - Color(0x61000000), - Color(0x00000000), - ], - ), - ), - ), - // Bottom gradient. - if (_theme(context).bottomButtonBar.isNotEmpty) - Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - stops: [ - 0.5, - 1.0, - ], - colors: [ - Color(0x00000000), - Color(0x61000000), - ], - ), - ), - ), - 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, - ), - ), - ), - ), - if (_theme(context).displaySeekBar) - Transform.translate( - offset: - _theme(context).bottomButtonBar.isNotEmpty - ? const Offset(0.0, 16.0) - : Offset.zero, - child: MaterialTvSeekBar( - onSeekStart: () { - _timer?.cancel(); - }, - onSeekEnd: () { - _timer = Timer( - _theme(context).controlsHoverDuration, - () { - if (mounted) { - setState(() { - visible = false; - }); - unshiftSubtitle(); - } - }, - ); - }, - ), - ), - if (_theme(context).bottomButtonBar.isNotEmpty) - Container( - height: _theme(context).buttonBarHeight, - margin: _theme(context).bottomButtonBarMargin, - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: _theme(context).bottomButtonBar, - ), - ), - ], - ), - ), - ], - ), - ), - // 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: Center( - child: TweenAnimationBuilder( - tween: Tween( - 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, - ), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } -} - -// SEEK BAR - -/// Material design seek bar. -class MaterialTvSeekBar extends StatefulWidget { - final VoidCallback? onSeekStart; - final VoidCallback? onSeekEnd; - - const MaterialTvSeekBar({ - Key? key, - this.onSeekStart, - this.onSeekEnd, - }) : super(key: key); - - @override - MaterialTvSeekBarState createState() => MaterialTvSeekBarState(); -} - -class MaterialTvSeekBarState extends State { - bool hover = false; - bool click = 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 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.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 (!click) 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() { - for (final subscription in subscriptions) { - subscription.cancel(); - } - super.dispose(); - } - - void onPointerMove(PointerMoveEvent e, BoxConstraints constraints) { - final percent = e.localPosition.dx / constraints.maxWidth; - setState(() { - hover = true; - slider = percent.clamp(0.0, 1.0); - }); - controller(context).player.seek(duration * slider); - } - - void onPointerDown() { - widget.onSeekStart?.call(); - setState(() { - click = true; - }); - } - - void onPointerUp() { - widget.onSeekEnd?.call(); - setState(() { - // Explicitly set the position to prevent the slider from jumping. - click = false; - position = duration * slider; - }); - controller(context).player.seek(duration * slider); - } - - void onHover(PointerHoverEvent e, BoxConstraints constraints) { - final percent = e.localPosition.dx / constraints.maxWidth; - setState(() { - hover = true; - slider = percent.clamp(0.0, 1.0); - }); - } - - void onEnter(PointerEnterEvent e, BoxConstraints constraints) { - final percent = e.localPosition.dx / constraints.maxWidth; - setState(() { - hover = true; - slider = percent.clamp(0.0, 1.0); - }); - } - - void onExit(PointerExitEvent e, BoxConstraints constraints) { - setState(() { - hover = false; - slider = 0.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); - } - } - - FocusNode focusNode = FocusNode(); - FocusNode focusNode2 = FocusNode(); - - @override - Widget build(BuildContext context) { - return FocusableActionDetector( - focusNode: focusNode, // Create a FocusNode to manage focus - autofocus: false, // Automatically focus when the widget appears - onFocusChange: (focused) { - // Handle focus changes - setState(() { - hover = focused; // Example: show hover effect when focused - }); - - if (focused) { - focusNode2.requestFocus(); - } - }, - child: Focus( - focusNode: focusNode2, - onKeyEvent: (node, event) { - if (event is KeyDownEvent) { - if (event.logicalKey == LogicalKeyboardKey.arrowRight) { - double percent = 0.01; - - double sliderPercent = - (positionPercent + percent).clamp(0.0, 1.0); - - setState(() { - hover = true; - slider = sliderPercent; - }); - controller(context).player.seek(duration * slider); - - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { - double percent = 0.01; - - double sliderPercent = - (positionPercent - percent).clamp(0.0, 1.0); - - setState(() { - hover = true; - slider = sliderPercent; - }); - controller(context).player.seek(duration * slider); - - return KeyEventResult.handled; - } - } - - return KeyEventResult.ignored; - }, - child: Container( - clipBehavior: Clip.none, - margin: _theme(context).seekBarMargin, - child: LayoutBuilder( - builder: (context, constraints) => Container( - color: const Color(0x00000000), - width: constraints.maxWidth, - height: _theme(context).seekBarContainerHeight, - child: Stack( - clipBehavior: Clip.none, - alignment: Alignment.centerLeft, - children: [ - AnimatedContainer( - width: constraints.maxWidth, - height: hover - ? _theme(context).seekBarHoverHeight - : _theme(context).seekBarHeight, - alignment: Alignment.centerLeft, - duration: _theme(context).seekBarThumbTransitionDuration, - color: _theme(context).seekBarColor, - child: Stack( - clipBehavior: Clip.none, - alignment: Alignment.centerLeft, - children: [ - Container( - width: constraints.maxWidth * slider, - color: _theme(context).seekBarHoverColor, - ), - Container( - width: constraints.maxWidth * bufferPercent, - color: _theme(context).seekBarBufferColor, - ), - Container( - width: click - ? constraints.maxWidth * slider - : constraints.maxWidth * positionPercent, - color: _theme(context).seekBarPositionColor, - ), - ], - ), - ), - Positioned( - left: click - ? (constraints.maxWidth - - _theme(context).seekBarThumbSize / 2) * - slider - : (constraints.maxWidth - - _theme(context).seekBarThumbSize / 2) * - positionPercent, - child: AnimatedContainer( - width: hover || click - ? _theme(context).seekBarThumbSize - : 0.0, - height: hover || click - ? _theme(context).seekBarThumbSize - : 0.0, - duration: _theme(context).seekBarThumbTransitionDuration, - decoration: BoxDecoration( - color: _theme(context).seekBarThumbColor, - borderRadius: BorderRadius.circular( - _theme(context).seekBarThumbSize / 2, - ), - ), - ), - ), - ], - ), - ), - ), - ), - ), - ); - } -} - -// BUTTON: PLAY/PAUSE - -/// A material design play/pause button. -class MaterialTvPlayOrPauseButton extends StatefulWidget { - /// Overriden icon size for [MaterialTvSkipPreviousButton]. - final double? iconSize; - - /// Overriden icon color for [MaterialTvSkipPreviousButton]. - final Color? iconColor; - - const MaterialTvPlayOrPauseButton({ - super.key, - this.iconSize, - this.iconColor, - }); - - @override - MaterialTvPlayOrPauseButtonState createState() => - MaterialTvPlayOrPauseButtonState(); -} - -class MaterialTvPlayOrPauseButtonState - extends State - with SingleTickerProviderStateMixin { - late final animation = AnimationController( - vsync: this, - value: controller(context).player.state.playing ? 1 : 0, - duration: const Duration(milliseconds: 200), - ); - - StreamSubscription? 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: AnimatedIcon( - progress: animation, - icon: AnimatedIcons.play_pause, - size: widget.iconSize ?? _theme(context).buttonBarButtonSize, - color: widget.iconColor ?? _theme(context).buttonBarButtonColor, - ), - ); - } -} - -// BUTTON: SKIP NEXT - -/// MaterialDesktop design skip next button. -class MaterialTvSkipNextButton extends StatelessWidget { - /// Icon for [MaterialTvSkipNextButton]. - final Widget? icon; - - /// Overriden icon size for [MaterialTvSkipNextButton]. - final double? iconSize; - - /// Overriden icon color for [MaterialTvSkipNextButton]. - final Color? iconColor; - - const MaterialTvSkipNextButton({ - Key? key, - this.icon, - this.iconSize, - this.iconColor, - }) : super(key: key); - - @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 - -/// MaterialDesktop design skip previous button. -class MaterialTvSkipPreviousButton extends StatelessWidget { - /// Icon for [MaterialTvSkipPreviousButton]. - final Widget? icon; - - /// Overriden icon size for [MaterialTvSkipPreviousButton]. - final double? iconSize; - - /// Overriden icon color for [MaterialTvSkipPreviousButton]. - final Color? iconColor; - - const MaterialTvSkipPreviousButton({ - Key? key, - this.icon, - this.iconSize, - this.iconColor, - }) : super(key: key); - - @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 - -/// MaterialDesktop design fullscreen button. -class MaterialTvFullscreenButton extends StatelessWidget { - /// Icon for [MaterialTvFullscreenButton]. - final Widget? icon; - - /// Overriden icon size for [MaterialTvFullscreenButton]. - final double? iconSize; - - /// Overriden icon color for [MaterialTvFullscreenButton]. - final Color? iconColor; - - const MaterialTvFullscreenButton({ - Key? key, - this.icon, - this.iconSize, - this.iconColor, - }) : super(key: key); - - @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 - -/// MaterialDesktop design custom button. -class MaterialTvCustomButton extends StatelessWidget { - /// Icon for [MaterialTvCustomButton]. - final Widget? icon; - - /// Icon size for [MaterialTvCustomButton]. - final double? iconSize; - - /// Icon color for [MaterialTvCustomButton]. - final Color? iconColor; - - /// The callback that is called when the button is tapped or otherwise activated. - final VoidCallback onPressed; - - const MaterialTvCustomButton({ - Key? key, - this.icon, - this.iconSize, - this.iconColor, - required this.onPressed, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return IconButton( - onPressed: onPressed, - icon: icon ?? const Icon(Icons.settings), - iconSize: iconSize ?? _theme(context).buttonBarButtonSize, - color: iconColor ?? _theme(context).buttonBarButtonColor, - ); - } -} - -// BUTTON: VOLUME - -/// MaterialDesktop design volume button & slider. -class MaterialTvVolumeButton extends StatefulWidget { - /// Icon size for the volume button. - final double? iconSize; - - /// Icon color for the volume button. - final Color? iconColor; - - /// Mute icon. - final Widget? volumeMuteIcon; - - /// Low volume icon. - final Widget? volumeLowIcon; - - /// High volume icon. - final Widget? volumeHighIcon; - - /// Width for the volume slider. - final double? sliderWidth; - - const MaterialTvVolumeButton({ - super.key, - this.iconSize, - this.iconColor, - this.volumeMuteIcon, - this.volumeLowIcon, - this.volumeHighIcon, - this.sliderWidth, - }); - - @override - MaterialTvVolumeButtonState createState() => MaterialTvVolumeButtonState(); -} - -class MaterialTvVolumeButtonState extends State - with SingleTickerProviderStateMixin { - late double volume = controller(context).player.state.volume; - - StreamSubscription? subscription; - - bool hover = false; - - bool mute = false; - double _volume = 0.0; - - @override - void setState(VoidCallback fn) { - if (mounted) { - super.setState(fn); - } - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - subscription ??= controller(context).player.stream.volume.listen((event) { - setState(() { - volume = event; - }); - }); - } - - @override - void dispose() { - subscription?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return MouseRegion( - onEnter: (e) { - setState(() { - hover = true; - }); - }, - onExit: (e) { - setState(() { - hover = false; - }); - }, - child: Listener( - onPointerSignal: (event) { - if (event is PointerScrollEvent) { - if (event.scrollDelta.dy < 0) { - controller(context).player.setVolume( - (volume + 5.0).clamp(0.0, 100.0), - ); - } - if (event.scrollDelta.dy > 0) { - controller(context).player.setVolume( - (volume - 5.0).clamp(0.0, 100.0), - ); - } - } - }, - child: Row( - children: [ - const SizedBox(width: 4.0), - IconButton( - onPressed: () async { - if (mute) { - await controller(context).player.setVolume(_volume); - mute = !mute; - } - // https://github.com/media-kit/media-kit/pull/250#issuecomment-1605588306 - else if (volume == 0.0) { - _volume = 100.0; - await controller(context).player.setVolume(100.0); - mute = false; - } else { - _volume = volume; - await controller(context).player.setVolume(0.0); - mute = !mute; - } - - setState(() {}); - }, - iconSize: widget.iconSize ?? - (_theme(context).buttonBarButtonSize * 0.8), - color: widget.iconColor ?? _theme(context).buttonBarButtonColor, - icon: AnimatedSwitcher( - duration: _theme(context).volumeBarTransitionDuration, - child: volume == 0.0 - ? (widget.volumeMuteIcon ?? - const Icon( - Icons.volume_off, - key: ValueKey(Icons.volume_off), - )) - : volume < 50.0 - ? (widget.volumeLowIcon ?? - const Icon( - Icons.volume_down, - key: ValueKey(Icons.volume_down), - )) - : (widget.volumeHighIcon ?? - const Icon( - Icons.volume_up, - key: ValueKey(Icons.volume_up), - )), - ), - ), - AnimatedOpacity( - opacity: hover ? 1.0 : 0.0, - duration: _theme(context).volumeBarTransitionDuration, - child: AnimatedContainer( - width: - hover ? (12.0 + (widget.sliderWidth ?? 52.0) + 18.0) : 12.0, - duration: _theme(context).volumeBarTransitionDuration, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - const SizedBox(width: 12.0), - SizedBox( - width: widget.sliderWidth ?? 52.0, - child: SliderTheme( - data: SliderThemeData( - trackHeight: 1.2, - inactiveTrackColor: _theme(context).volumeBarColor, - activeTrackColor: - _theme(context).volumeBarActiveColor, - thumbColor: _theme(context).volumeBarThumbColor, - thumbShape: RoundSliderThumbShape( - enabledThumbRadius: - _theme(context).volumeBarThumbSize / 2, - elevation: 0.0, - pressedElevation: 0.0, - ), - trackShape: _CustomTrackShape(), - overlayColor: const Color(0x00000000), - ), - child: Slider( - value: volume.clamp(0.0, 100.0), - min: 0.0, - max: 100.0, - onChanged: (value) async { - await controller(context).player.setVolume(value); - mute = false; - setState(() {}); - }, - ), - ), - ), - const SizedBox(width: 18.0), - ], - ), - ), - ), - ), - ], - ), - ), - ); - } -} - -// POSITION INDICATOR - -/// MaterialDesktop design position indicator. -class MaterialTvPositionIndicator extends StatefulWidget { - /// Overriden [TextStyle] for the [MaterialTvPositionIndicator]. - final TextStyle? style; - const MaterialTvPositionIndicator({super.key, this.style}); - - @override - MaterialTvPositionIndicatorState createState() => - MaterialTvPositionIndicatorState(); -} - -class MaterialTvPositionIndicatorState - extends State { - late Duration position = controller(context).player.state.position; - late Duration duration = controller(context).player.state.duration; - - final List 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 _CustomTrackShape extends RoundedRectSliderTrackShape { - @override - Rect getPreferredRect({ - required RenderBox parentBox, - Offset offset = Offset.zero, - required SliderThemeData sliderTheme, - bool isEnabled = false, - bool isDiscrete = false, - }) { - final height = sliderTheme.trackHeight; - final left = offset.dx; - final top = offset.dy + (parentBox.size.height - height!) / 2; - final width = parentBox.size.width; - return Rect.fromLTWH( - left, - top, - width, - height, - ); - } -} diff --git a/pubspec.lock b/pubspec.lock index 71e569f..69129d6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -392,6 +392,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.9" + media_kit_tv: + dependency: "direct main" + description: + name: media_kit_tv + sha256: e67fb5c4a7164bf2e6cf80b3876a40233e6910bf5b2487c17cfee110c86e4e95 + url: "https://pub.dev" + source: hosted + version: "0.0.1" media_kit_video: dependency: "direct main" description: @@ -958,5 +966,5 @@ packages: source: hosted version: "6.5.0" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.5.3 <4.0.0" flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 9e21cde..98cb007 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: media_kit: ^1.1.11 # Primary package. media_kit_video: ^1.2.5 # For video rendering. media_kit_libs_video: ^1.0.5 # Native video dependencies. + media_kit_tv: ^0.0.1 http: ^1.2.2 device_info_plus: ^11.0.0 cached_network_image: ^3.4.1