mirror of
https://github.com/immich-app/immich.git
synced 2026-03-26 20:00:44 +03:00
feat(mobile): consolidate video controls (#26673)
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. Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user