import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:overflow_view/overflow_view.dart'; import 'package:window_manager/window_manager.dart'; import 'package:fladder/models/media_playback_model.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/video_player/video_player.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/duration_extensions.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/refresh_state.dart'; import 'package:fladder/widgets/shared/fladder_slider.dart'; import 'package:fladder/widgets/shared/item_actions.dart'; const videoPlayerHeroTag = "HeroPlayer"; double floatingPlayerHeight(BuildContext context) => switch (AdaptiveLayout.viewSizeOf(context)) { ViewSize.phone => 75, ViewSize.tablet => 85, ViewSize.desktop => 95, }; class FloatingPlayerBar extends ConsumerStatefulWidget { const FloatingPlayerBar({super.key}); @override ConsumerState createState() => _CurrentlyPlayingBarState(); } class _CurrentlyPlayingBarState extends ConsumerState { bool showExpandButton = false; bool changingSliderValue = false; Duration lastPosition = Duration.zero; Future openFullScreenPlayer() async { setState(() => showExpandButton = false); ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(state: VideoPlayerState.fullScreen)); await Navigator.of(context, rootNavigator: true).push( MaterialPageRoute( builder: (context) => const VideoPlayer(), ), ); if (AdaptiveLayout.of(context).isDesktop || kIsWeb) { final fullScreen = await windowManager.isFullScreen(); if (fullScreen) { await windowManager.setFullScreen(false); } } if (context.mounted) { await context.refreshData(); } } Future stopPlayer() async => ref.read(videoPlayerProvider).stop(); @override Widget build(BuildContext context) { final playbackInfo = ref.watch(mediaPlaybackProvider); final player = ref.watch(videoPlayerProvider); final item = ref.watch(playBackModel.select((value) => value?.item)); if (!changingSliderValue) { lastPosition = playbackInfo.position; } var isFavourite = item?.userData.isFavourite == true; final isDesktop = AdaptiveLayout.of(context).isDesktop; final itemActions = [ ItemActionButton( label: Text(context.localized.audio), icon: Consumer( builder: (context, ref, child) { var volume = (player.lastState?.volume ?? 0) <= 0; return Icon( volume ? IconsaxPlusBold.volume_cross : IconsaxPlusBold.volume_high, ); }, ), action: () { final volume = player.lastState?.volume == 0 ? 100.0 : 0.0; player.setVolume(volume); }), ItemActionButton( label: Text(context.localized.stop), action: () async => stopPlayer(), icon: const Icon(IconsaxPlusBold.stop), ), ItemActionButton( label: Text(isFavourite ? context.localized.removeAsFavorite : context.localized.addAsFavorite), icon: Icon( color: isFavourite ? Colors.red : null, isFavourite ? IconsaxPlusBold.heart : IconsaxPlusLinear.heart, ), action: () async { final result = (await ref.read(userProvider.notifier).setAsFavorite( !isFavourite, item?.id ?? "", )) ?.body; if (result != null) { ref.read(playBackModel.notifier).update((state) => state?.updateUserData(result)); } }, ), ]; return Padding( padding: MediaQuery.paddingOf(context).copyWith(top: 0, bottom: isDesktop ? 0 : MediaQuery.paddingOf(context).bottom), child: Dismissible( key: const Key("CurrentlyPlayingBar"), confirmDismiss: (direction) async { if (direction == DismissDirection.up) { await openFullScreenPlayer(); } else { await stopPlayer(); } return false; }, direction: DismissDirection.vertical, child: InkWell( onLongPress: () => fladderSnackbar(context, title: "Swipe up/down to open/close the player"), child: Card( elevation: 5, color: Theme.of(context).colorScheme.primaryContainer, child: SizedBox( height: floatingPlayerHeight(context), child: LayoutBuilder(builder: (context, constraints) { return Column( mainAxisSize: MainAxisSize.min, children: [ Expanded( child: Padding( padding: const EdgeInsets.all(6), child: Row( spacing: 12, mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, children: [ if (playbackInfo.state == VideoPlayerState.minimized) Card( child: AspectRatio( aspectRatio: 1.67, child: MouseRegion( onEnter: (event) => setState(() => showExpandButton = true), onExit: (event) => setState(() => showExpandButton = false), child: Stack( children: [ Hero( tag: videoPlayerHeroTag, child: player.videoWidget( UniqueKey(), BoxFit.fitHeight, ) ?? const SizedBox.shrink(), ), Positioned.fill( child: Tooltip( message: "Expand player", waitDuration: const Duration(milliseconds: 500), child: AnimatedOpacity( opacity: showExpandButton ? 1 : 0, duration: const Duration(milliseconds: 125), child: Container( color: Colors.black.withValues(alpha: 0.6), child: FlatButton( onTap: () async => openFullScreenPlayer(), child: const Icon(Icons.keyboard_arrow_up_rounded), ), ), ), ), ) ], ), ), ), ), Expanded( child: InkWell( onTap: () => item?.navigateTo(context), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Flexible( child: Text( item?.title ?? "", style: Theme.of(context).textTheme.titleMedium, maxLines: 1, ), ), if (item?.detailedName(context)?.isNotEmpty == true) Flexible( child: Text( item?.detailedName(context) ?? "", overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.65), ), maxLines: 1, ), ), ], ), ), ), Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ if (constraints.maxWidth > 500) Flexible( child: Text( "${lastPosition.readAbleDuration} / ${playbackInfo.duration.readAbleDuration}"), ), Flexible( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: IconButton.filledTonal( onPressed: () => ref.read(videoPlayerProvider).playOrPause(), icon: playbackInfo.playing ? const Icon(Icons.pause_rounded) : const Icon(Icons.play_arrow_rounded), ), ), ), Flexible( child: OverflowView.flexible( builder: (context, remainingItemCount) => PopupMenuButton( iconColor: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45), padding: EdgeInsets.zero, itemBuilder: (context) => itemActions .sublist(itemActions.length - remainingItemCount) .map( (e) => e.toPopupMenuItem(useIcons: true), ) .toList(), ), children: itemActions.map((e) => e.toButton()).toList(), ), ) ], ), ) ], ), ), ), AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer ? SizedBox( height: 8, child: FladderSlider( value: lastPosition.inMilliseconds.toDouble(), min: 0.0, max: playbackInfo.duration.inMilliseconds.toDouble(), thumbWidth: 8, onChangeStart: (value) { setState(() { changingSliderValue = true; }); }, onChangeEnd: (value) async { await player.seek(Duration(milliseconds: value ~/ 1)); await Future.delayed(const Duration(milliseconds: 250)); if (player.lastState?.playing == true) { player.play(); } setState(() { lastPosition = Duration(milliseconds: value.toInt()); changingSliderValue = false; }); }, onChanged: (value) { setState(() { lastPosition = Duration(milliseconds: value.toInt()); }); }, ), ) : LinearProgressIndicator( minHeight: 8, backgroundColor: Colors.black.withValues(alpha: 0.25), color: Theme.of(context).colorScheme.primary, value: (playbackInfo.position.inMilliseconds / playbackInfo.duration.inMilliseconds) .clamp(0, 1), ) ], ); }), ), ), ), ), ); } }