diff --git a/mobile/lib/constants/colors.dart b/mobile/lib/constants/colors.dart index 069ed519cf..e39480de32 100644 --- a/mobile/lib/constants/colors.dart +++ b/mobile/lib/constants/colors.dart @@ -7,6 +7,6 @@ const String defaultColorPresetName = "indigo"; const Color immichBrandColorLight = Color(0xFF4150AF); const Color immichBrandColorDark = Color(0xFFACCBFA); -const Color whiteOpacity75 = Color.fromARGB((0.75 * 255) ~/ 1, 255, 255, 255); +const Color whiteOpacity75 = Color.fromRGBO(255, 255, 255, 0.75); const Color red400 = Color(0xFFEF5350); const Color grey200 = Color(0xFFEEEEEE); diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart index ea7ff51fa6..f413075da4 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart @@ -19,7 +19,6 @@ import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; @@ -248,11 +247,6 @@ class _AssetPageState extends ConsumerState { if (scaleState != PhotoViewScaleState.initial) { if (_dragStart == null) _viewer.setControls(false); - - final heroTag = ref.read(assetViewerProvider).currentAsset?.heroTag; - if (heroTag != null) { - ref.read(videoPlayerProvider(heroTag).notifier).pause(); - } return; } diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart index 113c55932f..cc171f4490 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -61,15 +61,27 @@ class ViewerBottomBar extends ConsumerWidget { ), ), child: Container( - color: Colors.black.withAlpha(125), - padding: EdgeInsets.only(bottom: context.padding.bottom, top: 16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (asset.isVideo) VideoControls(videoPlayerName: asset.heroTag), - if (!isReadonlyModeEnabled) - Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions), - ], + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [Colors.black45, Colors.black12, Colors.transparent], + stops: [0.0, 0.7, 1.0], + ), + ), + child: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.only(top: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (asset.isVideo) VideoControls(videoPlayerName: asset.heroTag), + if (!isReadonlyModeEnabled) + Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions), + ], + ), + ), ), ), ), diff --git a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart index ecfe0b3ddc..9285c01c41 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -10,7 +10,6 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; -import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; @@ -186,11 +185,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg final source = await _videoSource; if (source == null || !mounted) return; - unawaited( - nc.loadVideoSource(source).catchError((error) { - _log.severe('Error loading video source: $error'); - }), - ); + await _notifier.load(source); final loopVideo = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.loopVideo); await _notifier.setLoop(!widget.asset.isMotionPhoto && loopVideo); await _notifier.setVolume(1); @@ -213,21 +208,28 @@ class _NativeVideoViewerState extends ConsumerState with Widg @override Widget build(BuildContext context) { - // Prevent the provider from being disposed whilst the widget is alive. - ref.listen(videoPlayerProvider(widget.asset.heroTag), (_, __) {}); - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); + final status = ref.watch(videoPlayerProvider(widget.asset.heroTag).select((v) => v.status)); - return Stack( - children: [ - Center(child: widget.image), - if (!isCasting) - Visibility.maintain( - visible: _isVideoReady, - child: NativeVideoPlayerView(onViewReady: _initController), - ), - if (widget.showControls) Center(child: VideoViewerControls(asset: widget.asset)), - ], + return IgnorePointer( + child: Stack( + children: [ + Center(child: widget.image), + if (!isCasting) ...[ + Visibility.maintain( + visible: _isVideoReady, + child: NativeVideoPlayerView(onViewReady: _initController), + ), + Center( + child: AnimatedOpacity( + opacity: status == VideoPlaybackStatus.buffering ? 1.0 : 0.0, + duration: const Duration(milliseconds: 400), + child: const CircularProgressIndicator(), + ), + ), + ], + ], + ), ); } } diff --git a/mobile/lib/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart deleted file mode 100644 index e079f666ec..0000000000 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/models/cast/cast_manager_state.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/utils/hooks/timer_hook.dart'; -import 'package:immich_mobile/widgets/asset_viewer/center_play_button.dart'; -import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart'; - -class VideoViewerControls extends HookConsumerWidget { - final BaseAsset asset; - final Duration hideTimerDuration; - - const VideoViewerControls({super.key, required this.asset, this.hideTimerDuration = const Duration(seconds: 5)}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final videoPlayerName = asset.heroTag; - final assetIsVideo = asset.isVideo; - final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls && !s.showingDetails)); - final status = ref.watch(videoPlayerProvider(videoPlayerName).select((value) => value.status)); - - final cast = ref.watch(castProvider); - - // A timer to hide the controls - final hideTimer = useTimer(hideTimerDuration, () { - if (!context.mounted) { - return; - } - final status = ref.read(videoPlayerProvider(videoPlayerName)).status; - - // Do not hide on paused - if (status != VideoPlaybackStatus.paused && status != VideoPlaybackStatus.completed && assetIsVideo) { - ref.read(assetViewerProvider.notifier).setControls(false); - } - }); - final showBuffering = status == VideoPlaybackStatus.buffering && !cast.isCasting; - - /// Shows the controls and starts the timer to hide them - void showControlsAndStartHideTimer() { - hideTimer.reset(); - ref.read(assetViewerProvider.notifier).setControls(true); - } - - // When playback starts, reset the hide timer - ref.listen(videoPlayerProvider(videoPlayerName).select((v) => v.status), (previous, next) { - if (next == VideoPlaybackStatus.playing) { - hideTimer.reset(); - } - }); - - /// Toggles between playing and pausing depending on the state of the video - void togglePlay() { - showControlsAndStartHideTimer(); - - if (cast.isCasting) { - switch (cast.castState) { - case CastState.playing: - ref.read(castProvider.notifier).pause(); - case CastState.paused: - ref.read(castProvider.notifier).play(); - default: - } - return; - } - - final notifier = ref.read(videoPlayerProvider(videoPlayerName).notifier); - switch (status) { - case VideoPlaybackStatus.playing: - notifier.pause(); - case VideoPlaybackStatus.completed: - notifier.restart(); - default: - notifier.play(); - } - } - - void toggleControlsVisibility() { - if (showBuffering) return; - - if (showControls) { - ref.read(assetViewerProvider.notifier).setControls(false); - } else { - showControlsAndStartHideTimer(); - } - } - - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: toggleControlsVisibility, - child: IgnorePointer( - ignoring: !showControls, - child: Stack( - children: [ - if (showBuffering) - const Center(child: DelayedLoadingIndicator(fadeInDuration: Duration(milliseconds: 400))) - else - CenterPlayButton( - backgroundColor: Colors.black54, - iconColor: Colors.white, - isFinished: status == VideoPlaybackStatus.completed, - isPlaying: - status == VideoPlaybackStatus.playing || (cast.isCasting && cast.castState == CastState.playing), - show: assetIsVideo && showControls, - onPressed: togglePlay, - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart index 4ba4152a8d..397cd98ace 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart @@ -75,17 +75,29 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { child: AnimatedOpacity( opacity: opacity, duration: Durations.short2, - child: AppBar( - backgroundColor: showingDetails ? Colors.transparent : Colors.black.withValues(alpha: 0.5), - leading: const _AppBarBackButton(), - iconTheme: const IconThemeData(size: 22, color: Colors.white), - actionsIconTheme: const IconThemeData(size: 22, color: Colors.white), - shape: const Border(), - actions: showingDetails || isReadonlyModeEnabled - ? null - : isInLockedView - ? lockedViewActions - : actions, + child: DecoratedBox( + decoration: BoxDecoration( + gradient: showingDetails + ? null + : const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.black45, Colors.black12, Colors.transparent], + stops: [0.0, 0.7, 1.0], + ), + ), + child: AppBar( + backgroundColor: Colors.transparent, + leading: const _AppBarBackButton(), + iconTheme: const IconThemeData(size: 22, color: Colors.white), + actionsIconTheme: const IconThemeData(size: 22, color: Colors.white), + shape: const Border(), + actions: showingDetails || isReadonlyModeEnabled + ? null + : isInLockedView + ? lockedViewActions + : actions, + ), ), ), ); diff --git a/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart b/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart index 785dfd1e4c..19c92e7c96 100644 --- a/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart +++ b/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart @@ -100,11 +100,11 @@ class AssetViewerStateNotifier extends Notifier { return; } state = state.copyWith(showingDetails: showing, showingControls: showing ? true : state.showingControls); - if (showing) { - final heroTag = state.currentAsset?.heroTag; - if (heroTag != null) { - ref.read(videoPlayerProvider(heroTag).notifier).pause(); - } + + final heroTag = state.currentAsset?.heroTag; + if (heroTag != null) { + final notifier = ref.read(videoPlayerProvider(heroTag).notifier); + showing ? notifier.hold() : notifier.release(); } } diff --git a/mobile/lib/providers/asset_viewer/video_player_provider.dart b/mobile/lib/providers/asset_viewer/video_player_provider.dart index 0ca3bf4f74..a4a8bd1762 100644 --- a/mobile/lib/providers/asset_viewer/video_player_provider.dart +++ b/mobile/lib/providers/asset_viewer/video_player_provider.dart @@ -44,10 +44,7 @@ class VideoPlayerNotifier extends StateNotifier { NativeVideoPlayerController? _controller; Timer? _bufferingTimer; Timer? _seekTimer; - - void attachController(NativeVideoPlayerController controller) { - _controller = controller; - } + VideoPlaybackStatus? _holdStatus; @override void dispose() { @@ -59,6 +56,19 @@ class VideoPlayerNotifier extends StateNotifier { super.dispose(); } + void attachController(NativeVideoPlayerController controller) { + _controller = controller; + } + + Future load(VideoSource source) async { + _startBufferingTimer(); + try { + await _controller?.loadVideoSource(source); + } catch (e) { + _log.severe('Error loading video source: $e'); + } + } + Future pause() async { if (_controller == null) return; @@ -94,16 +104,50 @@ class VideoPlayerNotifier extends StateNotifier { } void seekTo(Duration position) { - if (_controller == null) return; + if (_controller == null || state.position == position) return; state = state.copyWith(position: position); - _seekTimer?.cancel(); - _seekTimer = Timer(const Duration(milliseconds: 100), () { - _controller?.seekTo(position.inMilliseconds); + if (_seekTimer?.isActive ?? false) return; + + _seekTimer = Timer(const Duration(milliseconds: 150), () { + _controller?.seekTo(state.position.inMilliseconds); }); } + void toggle() { + _holdStatus = null; + + switch (state.status) { + case VideoPlaybackStatus.paused: + play(); + case VideoPlaybackStatus.playing || VideoPlaybackStatus.buffering: + pause(); + case VideoPlaybackStatus.completed: + restart(); + } + } + + /// Pauses playback and preserves the current status for later restoration. + void hold() { + if (_holdStatus != null) return; + + _holdStatus = state.status; + pause(); + } + + /// Restores playback to the status before [hold] was called. + void release() { + final status = _holdStatus; + _holdStatus = null; + + switch (status) { + case VideoPlaybackStatus.playing || VideoPlaybackStatus.buffering: + play(); + default: + } + } + Future restart() async { seekTo(Duration.zero); await play(); @@ -149,13 +193,12 @@ class VideoPlayerNotifier extends StateNotifier { final position = Duration(milliseconds: playbackInfo.position); if (state.position == position) return; - if (state.status == VideoPlaybackStatus.buffering) { - state = state.copyWith(position: position, status: VideoPlaybackStatus.playing); - } else { - state = state.copyWith(position: position); - } + if (state.status == VideoPlaybackStatus.playing) _startBufferingTimer(); - _startBufferingTimer(); + state = state.copyWith( + position: position, + status: state.status == VideoPlaybackStatus.buffering ? VideoPlaybackStatus.playing : null, + ); } void onNativeStatusChanged() { @@ -173,9 +216,7 @@ class VideoPlayerNotifier extends StateNotifier { onNativePlaybackEnded(); } - if (state.status != newStatus) { - state = state.copyWith(status: newStatus); - } + if (state.status != newStatus) state = state.copyWith(status: newStatus); } void onNativePlaybackEnded() { @@ -186,7 +227,7 @@ class VideoPlayerNotifier extends StateNotifier { void _startBufferingTimer() { _bufferingTimer?.cancel(); _bufferingTimer = Timer(const Duration(seconds: 3), () { - if (mounted && state.status == VideoPlaybackStatus.playing) { + if (mounted && state.status != VideoPlaybackStatus.completed) { state = state.copyWith(status: VideoPlaybackStatus.buffering); } }); diff --git a/mobile/lib/providers/cast.provider.dart b/mobile/lib/providers/cast.provider.dart index 1cd5ded487..fea95f42aa 100644 --- a/mobile/lib/providers/cast.provider.dart +++ b/mobile/lib/providers/cast.provider.dart @@ -91,6 +91,16 @@ class CastNotifier extends StateNotifier { return discovered; } + void toggle() { + switch (state.castState) { + case CastState.playing: + pause(); + case CastState.paused: + play(); + default: + } + } + void play() { _gCastService.play(); } diff --git a/mobile/lib/widgets/asset_viewer/formatted_duration.dart b/mobile/lib/widgets/asset_viewer/formatted_duration.dart deleted file mode 100644 index fbcc8e6482..0000000000 --- a/mobile/lib/widgets/asset_viewer/formatted_duration.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/duration_extensions.dart'; - -class FormattedDuration extends StatelessWidget { - final Duration data; - const FormattedDuration(this.data, {super.key}); - - @override - Widget build(BuildContext context) { - return SizedBox( - width: data.inHours > 0 ? 70 : 60, // use a fixed width to prevent jitter - child: Text( - data.format(), - style: const TextStyle(fontSize: 14.0, color: Colors.white, fontWeight: FontWeight.w500), - textAlign: TextAlign.center, - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/video_controls.dart b/mobile/lib/widgets/asset_viewer/video_controls.dart index 381388d8d2..29e877b3dc 100644 --- a/mobile/lib/widgets/asset_viewer/video_controls.dart +++ b/mobile/lib/widgets/asset_viewer/video_controls.dart @@ -1,22 +1,110 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/asset_viewer/video_position.dart'; +import 'package:immich_mobile/constants/colors.dart'; +import 'package:immich_mobile/models/cast/cast_manager_state.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; +import 'package:immich_mobile/providers/cast.provider.dart'; +import 'package:immich_mobile/utils/hooks/timer_hook.dart'; +import 'package:immich_mobile/extensions/duration_extensions.dart'; +import 'package:immich_mobile/widgets/asset_viewer/animated_play_pause.dart'; -/// The video controls for the [videoPlayerProvider] -class VideoControls extends ConsumerWidget { +class VideoControls extends HookConsumerWidget { final String videoPlayerName; const VideoControls({super.key, required this.videoPlayerName}); + void _toggle(WidgetRef ref, bool isCasting) { + if (isCasting) { + ref.read(castProvider.notifier).toggle(); + } else { + ref.read(videoPlayerProvider(videoPlayerName).notifier).toggle(); + } + } + + void _onSeek(WidgetRef ref, bool isCasting, double value) { + final seekTo = Duration(microseconds: value.toInt()); + + if (isCasting) { + ref.read(castProvider.notifier).seekTo(seekTo); + return; + } + + ref.read(videoPlayerProvider(videoPlayerName).notifier).seekTo(seekTo); + } + @override Widget build(BuildContext context, WidgetRef ref) { - final isPortrait = context.orientation == Orientation.portrait; - return isPortrait - ? VideoPosition(videoPlayerName: videoPlayerName) - : Padding( - padding: const EdgeInsets.symmetric(horizontal: 60.0), - child: VideoPosition(videoPlayerName: videoPlayerName), - ); + final provider = videoPlayerProvider(videoPlayerName); + final cast = ref.watch(castProvider); + final isCasting = cast.isCasting; + + final (position, duration) = isCasting + ? ref.watch(castProvider.select((c) => (c.currentTime, c.duration))) + : ref.watch(provider.select((v) => (v.position, v.duration))); + + final videoStatus = ref.watch(provider.select((v) => v.status)); + final isPlaying = isCasting + ? cast.castState == CastState.playing + : videoStatus == VideoPlaybackStatus.playing || videoStatus == VideoPlaybackStatus.buffering; + final isFinished = !isCasting && videoStatus == VideoPlaybackStatus.completed; + + final hideTimer = useTimer(const Duration(seconds: 5), () { + if (!context.mounted) return; + if (ref.read(provider).status == VideoPlaybackStatus.playing) { + ref.read(assetViewerProvider.notifier).setControls(false); + } + }); + + ref.listen(provider.select((v) => v.status), (_, __) => hideTimer.reset()); + + final notifier = ref.read(provider.notifier); + final isLoaded = duration != Duration.zero; + + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + spacing: 16, + children: [ + Row( + children: [ + IconButton( + iconSize: 32, + padding: const EdgeInsets.all(12), + constraints: const BoxConstraints(), + icon: isFinished + ? const Icon(Icons.replay, color: Colors.white, size: 32) + : AnimatedPlayPause(color: Colors.white, size: 32, playing: isPlaying), + onPressed: () => _toggle(ref, isCasting), + ), + const Spacer(), + Text( + "${position.format()} / ${duration.format()}", + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + fontFeatures: [FontFeature.tabularFigures()], + ), + ), + const SizedBox(width: 16), + ], + ), + Slider( + value: min(position.inMicroseconds.toDouble(), duration.inMicroseconds.toDouble()), + min: 0, + max: max(duration.inMicroseconds.toDouble(), 1), + thumbColor: Colors.white, + activeColor: Colors.white, + inactiveColor: whiteOpacity75, + padding: EdgeInsets.zero, + onChangeStart: (_) => notifier.hold(), + onChangeEnd: (_) => notifier.release(), + onChanged: isLoaded ? (value) => _onSeek(ref, isCasting, value) : null, + ), + ], + ), + ); } } diff --git a/mobile/lib/widgets/asset_viewer/video_position.dart b/mobile/lib/widgets/asset_viewer/video_position.dart deleted file mode 100644 index cbcbdb88e7..0000000000 --- a/mobile/lib/widgets/asset_viewer/video_position.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/colors.dart'; -import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/widgets/asset_viewer/formatted_duration.dart'; - -class VideoPosition extends HookConsumerWidget { - final String videoPlayerName; - - const VideoPosition({super.key, required this.videoPlayerName}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isCasting = ref.watch(castProvider).isCasting; - - final (position, duration) = isCasting - ? ref.watch(castProvider.select((c) => (c.currentTime, c.duration))) - : ref.watch(videoPlayerProvider(videoPlayerName).select((v) => (v.position, v.duration))); - - final wasPlaying = useRef(true); - return duration == Duration.zero - ? const _VideoPositionPlaceholder() - : Column( - children: [ - Padding( - // align with slider's inherent padding - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [FormattedDuration(position), FormattedDuration(duration)], - ), - ), - Row( - children: [ - Expanded( - child: Slider( - value: min(position.inMicroseconds / duration.inMicroseconds * 100, 100), - min: 0, - max: 100, - thumbColor: Colors.white, - activeColor: Colors.white, - inactiveColor: whiteOpacity75, - onChangeStart: (value) { - final status = ref.read(videoPlayerProvider(videoPlayerName)).status; - wasPlaying.value = status != VideoPlaybackStatus.paused; - ref.read(videoPlayerProvider(videoPlayerName).notifier).pause(); - }, - onChangeEnd: (value) { - if (wasPlaying.value) { - ref.read(videoPlayerProvider(videoPlayerName).notifier).play(); - } - }, - onChanged: (value) { - final seekToDuration = (duration * (value / 100.0)); - - if (isCasting) { - ref.read(castProvider.notifier).seekTo(seekToDuration); - return; - } - - ref.read(videoPlayerProvider(videoPlayerName).notifier).seekTo(seekToDuration); - }, - ), - ), - ], - ), - ], - ); - } -} - -class _VideoPositionPlaceholder extends StatelessWidget { - const _VideoPositionPlaceholder(); - - static void _onChangedDummy(_) {} - - @override - Widget build(BuildContext context) { - return const Column( - children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [FormattedDuration(Duration.zero), FormattedDuration(Duration.zero)], - ), - ), - Row( - children: [ - Expanded( - child: Slider( - value: 0.0, - min: 0, - max: 100, - thumbColor: Colors.white, - activeColor: Colors.white, - inactiveColor: whiteOpacity75, - onChanged: _onChangedDummy, - ), - ), - ], - ), - ], - ); - } -}