Fladder/lib/screens/video_player/components/video_player_next_wrapper.dart
PartyDonut da354437e3
feature: Added LibMDK video player backend (#162)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
2024-11-22 18:53:31 +01:00

467 lines
17 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:ficonsax/ficonsax.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:screen_brightness/screen_brightness.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/movie_model.dart';
import 'package:fladder/models/media_playback_model.dart';
import 'package:fladder/models/playback/playback_model.dart';
import 'package:fladder/models/settings/video_player_settings.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/screens/shared/animated_fade_size.dart';
import 'package:fladder/screens/shared/default_title_bar.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/fladder_image.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart';
import 'package:fladder/widgets/shared/full_screen_button.dart'
if (dart.library.html) 'package:fladder/widgets/shared/full_screen_button_web.dart';
import 'package:fladder/widgets/shared/progress_floating_button.dart';
class VideoPlayerNextWrapper extends ConsumerStatefulWidget {
final Widget video;
final Widget controls;
final List<Widget> overlays;
const VideoPlayerNextWrapper({
required this.video,
required this.controls,
this.overlays = const [],
super.key,
});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _VideoPlayerNextWrapperState();
}
class _VideoPlayerNextWrapperState extends ConsumerState<VideoPlayerNextWrapper> {
bool show = false;
bool showOverwrite = false;
late RestartableTimerController timerController =
RestartableTimerController(const Duration(seconds: 30), const Duration(milliseconds: 33), onTimeout: onTimeOut);
void onTimeOut() {
timerController.cancel();
if (showOverwrite == true) return;
final nextUp = ref.read(playBackModel.select((value) => value?.nextVideo));
if (nextUp != null) {
ref.read(playbackModelHelper).loadNewVideo(nextUp);
}
hideNextUp();
}
void showNextScreen(MediaPlaybackModel model) {
final nextUp = ref.read(playBackModel.select((value) => value?.nextVideo));
if (nextUp == null) return;
if (show) return;
if (showOverwrite) return;
if (!model.playing) return;
if (model.buffering) return;
setState(() {
show = true;
timerController.reset();
timerController.play();
});
}
void determineShow(MediaPlaybackModel model) {
final playerState = ref.watch(mediaPlaybackProvider.select((value) => value.state));
if (playerState != VideoPlayerState.fullScreen) {
showOverwrite = false;
show = false;
return;
}
final nextType = ref.read(videoPlayerSettingsProvider.select((value) => value.nextVideoType));
if (nextType == AutoNextType.off || model.duration < const Duration(seconds: 40)) {
showOverwrite = false;
show = false;
return;
}
final credits = ref.read(playBackModel)?.mediaSegments?.outro;
if (nextType == AutoNextType.static || credits == null) {
if ((model.duration - model.position).abs() < const Duration(seconds: 32)) {
showNextScreen(model);
return;
}
} else if (nextType == AutoNextType.smart) {
final maxTime = ref.read(userProvider.select((value) => value?.serverConfiguration?.maxResumePct ?? 90));
final resumeDuration = model.duration * (maxTime / 100);
final timeLeft = model.duration - credits.end;
if (credits.end > resumeDuration && timeLeft < const Duration(seconds: 30)) {
if (model.position >= credits.start) {
showNextScreen(model);
return;
}
} else if ((model.duration - model.position).abs() < const Duration(seconds: 32)) {
showNextScreen(model);
return;
}
}
setState(() {
show = false;
showOverwrite = false;
timerController.cancel();
});
}
void hideNextUp() {
timerController.cancel();
setState(() {
show = false;
showOverwrite = true;
});
}
Future<void> closePlayer() async {
clearOverlaySettings();
ref.read(videoPlayerProvider).stop();
Navigator.of(context).pop();
}
Future<void> clearOverlaySettings() async {
if (AdaptiveLayout.of(context).inputDevice != InputDevice.pointer) {
ScreenBrightness().resetScreenBrightness();
} else {
closeFullScreen();
}
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarIconBrightness: ref.read(clientSettingsProvider.select((value) => value.statusBarBrightness(context))),
));
}
@override
void dispose() {
timerController.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
const animSpeed = Duration(milliseconds: 250);
final nextUp = ref.watch(playBackModel.select((value) => value?.nextVideo));
final currentItem = ref.watch(playBackModel.select((value) => value?.item));
final portraitMode = MediaQuery.sizeOf(context).width < MediaQuery.sizeOf(context).height;
double padding = show ? 16 : 0;
ref.listen(mediaPlaybackProvider, (previous, next) => determineShow(next));
return Hero(
tag: videoPlayerHeroTag,
child: Stack(
children: [
Container(
color: Theme.of(context).colorScheme.surfaceContainerLowest.withOpacity(0.2),
),
if (nextUp != null)
AnimatedAlign(
duration: animSpeed,
alignment: portraitMode ? Alignment.bottomCenter : Alignment.centerRight,
child: AnimatedOpacity(
duration: animSpeed,
opacity: show ? 1 : 0,
child: Padding(
padding: MediaQuery.paddingOf(context).add(const EdgeInsets.all(32)),
child: FractionallySizedBox(
widthFactor: portraitMode ? null : 0.35,
heightFactor: portraitMode ? 0.5 : null,
child: Card(
elevation: 10,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: Text(
context.localized.nextUp,
softWrap: false,
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
.titleLarge
?.copyWith(fontWeight: FontWeight.bold, fontSize: 24.0),
),
),
SizedBox.square(
dimension: 45.0,
child: ProgressFloatingButton(
controller: timerController,
),
),
].addInBetween(
const SizedBox(
height: 16,
width: 16,
),
),
),
const Divider(),
Flexible(
child: SingleChildScrollView(
child: _NextUpInformation(
item: nextUp,
),
),
),
].addInBetween(const SizedBox(
height: 8,
width: 8,
)),
),
),
),
),
),
),
),
AnimatedAlign(
duration: animSpeed,
alignment: portraitMode ? Alignment.topCenter : Alignment.centerLeft,
child: AnimatedPadding(
duration: animSpeed,
padding: EdgeInsets.all(padding).add(show ? MediaQuery.paddingOf(context) : EdgeInsets.zero),
child: AnimatedFractionallySizedBox(
duration: animSpeed,
heightFactor: show ? (portraitMode ? 0.40 : 0.9) : 1.0,
widthFactor: show ? (portraitMode ? 1 : 0.60) : 1.0,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (currentItem != null)
AnimatedFadeSize(
duration: animSpeed,
child: show
? Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
children: [
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: Text(currentItem.title,
style: Theme.of(context).textTheme.displaySmall)),
if (currentItem.label(context) != null)
Flexible(
child: Text(
currentItem.label(context)!,
maxLines: 2,
overflow: TextOverflow.fade,
style: Theme.of(context).textTheme.bodyMedium,
),
),
],
),
),
const SizedBox(width: 8),
IconButton.filledTonal(
onPressed: () => hideNextUp(),
tooltip: context.localized.resumeVideo,
icon: const Icon(IconsaxBold.maximize_4),
),
const SizedBox(width: 8),
IconButton.filledTonal(
onPressed: () => closePlayer(),
tooltip: context.localized.closeVideo,
icon: const Icon(IconsaxBold.close_square),
),
],
),
)
: const SizedBox.shrink(),
),
Flexible(
child: Stack(
fit: StackFit.passthrough,
children: [
AnimatedContainer(
duration: animSpeed,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(show ? 16 : 0),
),
child: widget.video,
),
IgnorePointer(
ignoring: show,
child: AnimatedOpacity(
opacity: show ? 0 : 1,
duration: animSpeed,
child: widget.controls,
),
),
],
),
),
IgnorePointer(
ignoring: !show,
child: AnimatedFadeSize(
duration: animSpeed,
child: show
? Padding(
padding: const EdgeInsets.only(top: 16),
child: _SimpleControls(
skip: nextUp != null ? () => onTimeOut() : null,
),
)
: const SizedBox.shrink(),
),
),
],
),
),
),
),
if (AdaptiveLayout.of(context).isDesktop)
IgnorePointer(
ignoring: !show,
child: AnimatedOpacity(
duration: animSpeed,
opacity: show ? 1 : 0,
child: const Align(
alignment: Alignment.topRight,
child: DefaultTitleBar(),
),
),
),
],
),
);
}
}
class _NextUpInformation extends StatelessWidget {
final ItemBaseModel item;
const _NextUpInformation({
required this.item,
});
@override
Widget build(BuildContext context) {
return switch (item) {
MovieModel _ => Row(
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 150),
child: AspectRatio(
aspectRatio: 0.67,
child: Card(
child: FladderImage(
image: item.images?.primary,
),
),
),
),
),
Text(
item.title,
style: Theme.of(context).textTheme.titleLarge,
),
].addInBetween(
const SizedBox(height: 8),
),
),
Flexible(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.localized.overview,
style: Theme.of(context).textTheme.titleLarge,
),
const Divider(),
Text(item.overview.summary),
],
),
)
].addInBetween(
const SizedBox(width: 16),
),
),
_ => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
style: Theme.of(context).textTheme.titleLarge,
),
if (item.label(context) != null)
Text(
item.label(context)!,
),
Flexible(
child: AspectRatio(
aspectRatio: 2.1,
child: Card(
child: FladderImage(
image: item.images?.primary,
),
),
),
),
Text(
context.localized.overview,
style: Theme.of(context).textTheme.titleLarge,
),
Text(item.overview.summary),
const SizedBox(height: 12)
].addInBetween(
const SizedBox(height: 8),
),
)
};
}
}
class _SimpleControls extends ConsumerWidget {
final Function()? skip;
const _SimpleControls({
this.skip,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(videoPlayerProvider);
final isPlaying = ref.watch(mediaPlaybackProvider.select((value) => value.playing));
return Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton.filledTonal(
onPressed: () => player.playOrPause(),
icon: Icon(isPlaying ? IconsaxBold.pause : IconsaxBold.play),
),
if (skip != null)
IconButton.filledTonal(
onPressed: skip,
tooltip: context.localized.playNextVideo,
icon: const Icon(IconsaxBold.next),
)
].addInBetween(const SizedBox(width: 4)));
}
}