mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
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>
110 lines
4 KiB
Dart
110 lines
4 KiB
Dart
import 'dart:math';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.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';
|
|
|
|
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 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,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|