import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:path/path.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:window_manager/window_manager.dart'; import 'package:fladder/models/items/photos_model.dart'; import 'package:fladder/models/settings/video_player_settings.dart'; import 'package:fladder/providers/settings/photo_view_settings_provider.dart'; import 'package:fladder/providers/settings/video_player_settings_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/util/duration_extensions.dart'; import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/widgets/shared/fladder_slider.dart'; import 'package:fladder/wrappers/players/base_player.dart'; import 'package:fladder/wrappers/players/lib_mdk.dart' if (dart.library.html) 'package:fladder/stubs/web/lib_mdk_web.dart'; import 'package:fladder/wrappers/players/lib_mpv.dart'; class SimpleVideoPlayer extends ConsumerStatefulWidget { final PhotoModel video; final bool showOverlay; final VoidCallback onTapped; const SimpleVideoPlayer({required this.video, required this.showOverlay, required this.onTapped, super.key}); @override ConsumerState createState() => _SimpleVideoPlayerState(); } class _SimpleVideoPlayerState extends ConsumerState with WindowListener, WidgetsBindingObserver { late final BasePlayer player = switch (ref.read(videoPlayerSettingsProvider.select((value) => value.wantedPlayer))) { PlayerOptions.libMDK => LibMDK(), PlayerOptions.libMPV => LibMPV(), _ => LibMDK(), }; late String videoUrl = ""; bool playing = false; bool wasPlaying = false; Duration position = Duration.zero; Duration lastPosition = Duration.zero; Duration duration = Duration.zero; List subscriptions = []; @override void didChangeAppLifecycleState(AppLifecycleState state) { switch (state) { case AppLifecycleState.resumed: if (playing) player.play(); break; case AppLifecycleState.hidden: case AppLifecycleState.paused: case AppLifecycleState.detached: if (playing) player.pause(); break; default: break; } } @override void initState() { super.initState(); windowManager.addListener(this); WidgetsBinding.instance.addObserver(this); playing = player.lastState.playing; position = player.lastState.position; duration = player.lastState.duration; WidgetsBinding.instance.addPostFrameCallback((value) async { await Future.delayed(const Duration(milliseconds: 500)); if (mounted) _init(); }); } @override void onWindowMinimize() { if (playing) player.pause(); super.onWindowMinimize(); } void _init() async { final Map directOptions = { 'Static': 'true', 'mediaSourceId': widget.video.id, 'api_key': ref.read(userProvider)?.credentials.token, }; final params = Uri(queryParameters: directOptions).query; player.init(ref.read(videoPlayerSettingsProvider)); videoUrl = joinAll([ref.read(userProvider)?.server ?? "", "Videos", widget.video.id, "stream?$params"]); subscriptions.add(player.stateStream.listen((event) { setState(() { playing = event.playing; position = event.position; duration = event.duration; }); })); await player.loadVideo(videoUrl, !ref.watch(photoViewSettingsProvider).autoPlay); await player.setVolume(ref.watch(photoViewSettingsProvider.select((value) => value.mute)) ? 0 : 100); await player.loop(ref.watch(photoViewSettingsProvider.select((value) => value.repeat))); } @override void dispose() { Future.microtask(() async { await player.dispose(); }); for (final s in subscriptions) { s.cancel(); } WidgetsBinding.instance.removeObserver(this); windowManager.removeListener(this); super.dispose(); } @override Widget build(BuildContext context) { final textStyle = Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold); ref.listen( photoViewSettingsProvider.select((value) => value.repeat), (previous, next) => player.loop(next), ); ref.listen( photoViewSettingsProvider.select((value) => value.mute), (previous, next) => player.setVolume(next ? 0 : 100), ); return GestureDetector( onTap: widget.onTapped, child: Stack( alignment: Alignment.center, children: [ Positioned.fill( child: FladderImage( image: widget.video.thumbnail?.primary, disableBlur: true, fit: BoxFit.contain, ), ), //Fixes small overlay problems with thumbnail Transform.scale( scaleY: 1.004, child: player.videoWidget( UniqueKey(), BoxFit.contain, ), ), IgnorePointer( ignoring: !widget.showOverlay, child: AnimatedOpacity( opacity: widget.showOverlay ? 1 : 0, duration: const Duration(milliseconds: 250), child: Stack( fit: StackFit.expand, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 12) .add(EdgeInsets.only(bottom: 80 + MediaQuery.of(context).padding.bottom)), child: Align( alignment: Alignment.bottomCenter, child: SizedBox( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Flexible( child: Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ SizedBox( height: 40, child: FladderSlider( min: 0.0, max: duration.inMilliseconds.toDouble(), value: position.inMilliseconds.toDouble().clamp( 0, duration.inMilliseconds.toDouble(), ), onChangeEnd: (e) async { await player.seek(Duration(milliseconds: e ~/ 1)); if (wasPlaying) { player.play(); } }, onChangeStart: (value) { wasPlaying = player.lastState.playing; player.pause(); }, onChanged: (e) { setState(() => position = Duration(milliseconds: e ~/ 1)); }, ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( children: [ Text(position.readAbleDuration, style: textStyle), const Spacer(), Text((duration - position).readAbleDuration, style: textStyle), ], ), ), ], ), ), const SizedBox(width: 16), IconButton( color: Theme.of(context).colorScheme.onSurface, onPressed: () async { await player.playOrPause(); if (player.lastState.playing) { WakelockPlus.enable(); } else { WakelockPlus.disable(); } }, icon: Icon( player.lastState.playing ? IconsaxPlusBold.pause_circle : IconsaxPlusBold.play_circle, ), ) ], ), ), ), ), ], ), ), ), ], ), ); } }