mirror of
https://github.com/immich-app/immich.git
synced 2026-04-18 23:08:53 -04:00
We are generally looking to move away from hooks as they are hard to reason about and have weird bugs. In particular, the timer did not properly capture the ref of the callback, and so it would execute on old state. A standard stateful widget does not have this problem, and is easier to organise. Co-authored-by: Alex <alex.tran1502@gmail.com>
151 lines
5.0 KiB
Dart
151 lines
5.0 KiB
Dart
import 'dart:math';
|
|
|
|
import 'package:async/async.dart';
|
|
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/extensions/duration_extensions.dart';
|
|
import 'package:immich_mobile/widgets/asset_viewer/animated_play_pause.dart';
|
|
|
|
class VideoControls extends ConsumerStatefulWidget {
|
|
final String videoPlayerName;
|
|
|
|
static const List<Shadow> _controlShadows = [Shadow(color: Colors.black87, blurRadius: 6, offset: Offset(0, 1))];
|
|
|
|
const VideoControls({super.key, required this.videoPlayerName});
|
|
|
|
@override
|
|
ConsumerState<VideoControls> createState() => _VideoControlsState();
|
|
}
|
|
|
|
class _VideoControlsState extends ConsumerState<VideoControls> {
|
|
late final RestartableTimer _hideTimer;
|
|
|
|
AutoDisposeStateNotifierProvider<VideoPlayerNotifier, VideoPlayerState> get _provider =>
|
|
videoPlayerProvider(widget.videoPlayerName);
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_hideTimer = RestartableTimer(const Duration(seconds: 5), _onHideTimer);
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant VideoControls oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.videoPlayerName != widget.videoPlayerName) {
|
|
_hideTimer.reset();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_hideTimer.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
void _onHideTimer() {
|
|
if (!mounted) return;
|
|
if (ref.read(_provider).status == VideoPlaybackStatus.playing) {
|
|
ref.read(assetViewerProvider.notifier).setControls(false);
|
|
}
|
|
}
|
|
|
|
void _toggle(bool isCasting) {
|
|
if (isCasting) {
|
|
ref.read(castProvider.notifier).toggle();
|
|
return;
|
|
}
|
|
|
|
ref.read(_provider.notifier).toggle();
|
|
}
|
|
|
|
void _onSeek(bool isCasting, double value) {
|
|
final seekTo = Duration(microseconds: value.toInt());
|
|
|
|
if (isCasting) {
|
|
ref.read(castProvider.notifier).seekTo(seekTo);
|
|
return;
|
|
}
|
|
|
|
ref.read(_provider.notifier).seekTo(seekTo);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
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;
|
|
|
|
ref.listen(assetViewerProvider.select((v) => v.showingControls), (prev, showing) {
|
|
if (showing && prev != showing) _hideTimer.reset();
|
|
});
|
|
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.only(left: 16, right: 16, bottom: 12),
|
|
child: Column(
|
|
spacing: 4,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
IconButton(
|
|
iconSize: 32,
|
|
padding: const EdgeInsets.all(12),
|
|
constraints: const BoxConstraints(),
|
|
icon: isFinished
|
|
? const Icon(Icons.replay, color: Colors.white, shadows: VideoControls._controlShadows)
|
|
: AnimatedPlayPause(
|
|
color: Colors.white,
|
|
playing: isPlaying,
|
|
shadows: VideoControls._controlShadows,
|
|
),
|
|
onPressed: () => _toggle(isCasting),
|
|
),
|
|
const Spacer(),
|
|
Text(
|
|
"${position.format()} / ${duration.format()}",
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w500,
|
|
fontFeatures: [FontFeature.tabularFigures()],
|
|
shadows: VideoControls._controlShadows,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
],
|
|
),
|
|
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(isCasting, value) : null,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|