From e501cae45d949994dee3f9d027b0b2d0d927f11d Mon Sep 17 00:00:00 2001 From: Thomas Way Date: Thu, 5 Mar 2026 12:28:18 +0000 Subject: [PATCH] feat(mobile): consolidate video controls Videos have recently been changed to support zooming, but this can make the controls in the centre of the screen unergonomic as they will either stay in the centre when dismissing, or stick to the video when zooming. Neither is great. We should align the behaviour with other apps which has the play/pause toggle at the bottom of the screen with the seeker bar instead. --- mobile/lib/constants/colors.dart | 2 +- .../asset_viewer/asset_page.widget.dart | 6 - .../asset_viewer/bottom_bar.widget.dart | 30 +++-- .../asset_viewer/video_viewer.widget.dart | 40 +++--- .../video_viewer_controls.widget.dart | 114 ------------------ .../viewer_top_app_bar.widget.dart | 34 ++++-- .../asset_viewer/asset_viewer.provider.dart | 10 +- .../asset_viewer/video_player_provider.dart | 77 +++++++++--- mobile/lib/providers/cast.provider.dart | 10 ++ .../asset_viewer/formatted_duration.dart | 19 --- .../widgets/asset_viewer/video_controls.dart | 110 +++++++++++++++-- .../widgets/asset_viewer/video_position.dart | 110 ----------------- 12 files changed, 239 insertions(+), 323 deletions(-) delete mode 100644 mobile/lib/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart delete mode 100644 mobile/lib/widgets/asset_viewer/formatted_duration.dart delete mode 100644 mobile/lib/widgets/asset_viewer/video_position.dart 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, - ), - ), - ], - ), - ], - ); - } -}