mirror of
https://github.com/gabehf/Fladder.git
synced 2026-04-23 20:41:51 -07:00
feature: Added LibMDK video player backend (#162)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
6e32018183
commit
da354437e3
53 changed files with 1499 additions and 1006 deletions
|
|
@ -37,7 +37,7 @@ class MediaStreamInformation extends ConsumerWidget {
|
|||
_StreamOptionSelect(
|
||||
label: Text(context.localized.audio),
|
||||
current: mediaStream.currentAudioStream?.displayTitle ?? "",
|
||||
itemBuilder: (context) => mediaStream.audioStreams
|
||||
itemBuilder: (context) => [AudioStreamModel.no(), ...mediaStream.audioStreams]
|
||||
.map(
|
||||
(e) => PopupMenuItem(
|
||||
value: e,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import 'package:fladder/models/account_model.dart';
|
|||
import 'package:fladder/providers/auth_provider.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/screens/shared/user_icon.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
|
||||
class LoginUserGrid extends ConsumerWidget {
|
||||
|
|
@ -37,79 +36,87 @@ class LoginUserGrid extends ConsumerWidget {
|
|||
itemCount: users.length,
|
||||
itemBuilder: (context, index) {
|
||||
final user = users[index];
|
||||
return _CardHolder(
|
||||
return FlatButton(
|
||||
key: Key(user.id),
|
||||
content: Stack(
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: UserIcon(
|
||||
labelStyle: Theme.of(context).textTheme.headlineMedium,
|
||||
user: user,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
onTap: () => editMode ? onLongPress?.call(user) : onPressed?.call(user),
|
||||
onLongPress: () => onLongPress?.call(user),
|
||||
child: _CardHolder(
|
||||
content: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
user.authMethod.icon,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
user.name,
|
||||
maxLines: 2,
|
||||
softWrap: true,
|
||||
)),
|
||||
],
|
||||
),
|
||||
if (user.credentials.serverName.isNotEmpty)
|
||||
Opacity(
|
||||
opacity: 0.75,
|
||||
child: Row(
|
||||
child: UserIcon(
|
||||
labelStyle: Theme.of(context).textTheme.headlineMedium,
|
||||
user: user,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
const Icon(
|
||||
IconsaxBold.driver_2,
|
||||
size: 14,
|
||||
Icon(
|
||||
user.authMethod.icon,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
user.credentials.serverName,
|
||||
maxLines: 2,
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
user.name,
|
||||
maxLines: 2,
|
||||
softWrap: true,
|
||||
)),
|
||||
],
|
||||
),
|
||||
)
|
||||
].addInBetween(const SizedBox(width: 4, height: 4)),
|
||||
),
|
||||
if (editMode)
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Card(
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
IconsaxBold.edit_2,
|
||||
size: 14,
|
||||
if (user.credentials.serverName.isNotEmpty)
|
||||
Opacity(
|
||||
opacity: 0.75,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
const Icon(
|
||||
IconsaxBold.driver_2,
|
||||
size: 14,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
user.credentials.serverName,
|
||||
maxLines: 2,
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
].addInBetween(const SizedBox(width: 4, height: 4)),
|
||||
),
|
||||
),
|
||||
if (editMode)
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Card(
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
IconsaxBold.edit_2,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () => editMode ? onLongPress?.call(user) : onPressed?.call(user),
|
||||
onLongPress: () => onLongPress?.call(user),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -118,13 +125,9 @@ class LoginUserGrid extends ConsumerWidget {
|
|||
|
||||
class _CardHolder extends StatelessWidget {
|
||||
final Widget content;
|
||||
final Function() onTap;
|
||||
final Function() onLongPress;
|
||||
|
||||
const _CardHolder({
|
||||
required this.content,
|
||||
required this.onTap,
|
||||
required this.onLongPress,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -137,14 +140,7 @@ class _CardHolder extends StatelessWidget {
|
|||
margin: EdgeInsets.zero,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 150, maxWidth: 150),
|
||||
child: FlatButton(
|
||||
onTap: onTap,
|
||||
onLongPress: AdaptiveLayout.of(context).isDesktop ? onLongPress : null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
child: content,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,24 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:ficonsax/ficonsax.dart';
|
||||
import 'package:fladder/providers/settings/photo_view_settings_provider.dart';
|
||||
import 'package:fladder/util/fladder_image.dart';
|
||||
import 'package:fladder/widgets/shared/fladder_slider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
|
||||
import 'package:fladder/models/items/photos_model.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/util/duration_extensions.dart';
|
||||
import 'package:ficonsax/ficonsax.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.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/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;
|
||||
|
|
@ -26,10 +30,10 @@ class SimpleVideoPlayer extends ConsumerStatefulWidget {
|
|||
}
|
||||
|
||||
class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with WindowListener, WidgetsBindingObserver {
|
||||
final Player player = Player(
|
||||
configuration: const PlayerConfiguration(title: "nl.jknaapen.fladder", libass: true),
|
||||
);
|
||||
late VideoController controller = VideoController(player);
|
||||
late final player = switch (ref.read(videoPlayerSettingsProvider.select((value) => value.wantedPlayer))) {
|
||||
PlayerOptions.libMDK => LibMDK(),
|
||||
PlayerOptions.libMPV => LibMPV(),
|
||||
};
|
||||
late String videoUrl = "";
|
||||
|
||||
bool playing = false;
|
||||
|
|
@ -61,9 +65,9 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
|
|||
super.initState();
|
||||
windowManager.addListener(this);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
playing = player.state.playing;
|
||||
position = player.state.position;
|
||||
duration = player.state.duration;
|
||||
playing = player.lastState.playing;
|
||||
position = player.lastState.position;
|
||||
duration = player.lastState.duration;
|
||||
Future.microtask(() async => {_init()});
|
||||
}
|
||||
|
||||
|
|
@ -84,43 +88,21 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
|
|||
|
||||
videoUrl = '${ref.read(userProvider)?.server ?? ""}/Videos/${widget.video.id}/stream?$params';
|
||||
|
||||
subscriptions.addAll(
|
||||
[
|
||||
player.stream.playing.listen((event) {
|
||||
setState(() {
|
||||
playing = event;
|
||||
});
|
||||
if (playing) {
|
||||
WakelockPlus.enable();
|
||||
} else {
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
}),
|
||||
player.stream.position.listen((event) {
|
||||
setState(() {
|
||||
position = event;
|
||||
});
|
||||
}),
|
||||
player.stream.completed.listen((event) {
|
||||
if (event) {
|
||||
_restartVideo();
|
||||
}
|
||||
}),
|
||||
player.stream.duration.listen((event) {
|
||||
setState(() {
|
||||
duration = event;
|
||||
});
|
||||
}),
|
||||
],
|
||||
);
|
||||
subscriptions.add(player.stateStream.listen((event) {
|
||||
setState(() {
|
||||
playing = event.playing;
|
||||
position = event.position;
|
||||
duration = event.duration;
|
||||
});
|
||||
if (playing) {
|
||||
WakelockPlus.enable();
|
||||
} else {
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
}));
|
||||
await player.open(videoUrl, !ref.watch(photoViewSettingsProvider).autoPlay);
|
||||
await player.loop(ref.watch(photoViewSettingsProvider.select((value) => value.repeat)));
|
||||
await player.setVolume(ref.watch(photoViewSettingsProvider.select((value) => value.mute)) ? 0 : 100);
|
||||
await player.open(Media(videoUrl), play: !ref.watch(photoViewSettingsProvider).autoPlay);
|
||||
}
|
||||
|
||||
void _restartVideo() {
|
||||
if (ref.read(photoViewSettingsProvider.select((value) => value.repeat))) {
|
||||
player.play();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -142,6 +124,9 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
|
|||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold, shadows: [const Shadow(blurRadius: 2)]);
|
||||
ref.listen(photoViewSettingsProvider.select((value) => value.repeat), (previous, next) {
|
||||
player.loop(next);
|
||||
});
|
||||
ref.listen(
|
||||
photoViewSettingsProvider.select((value) => value.mute),
|
||||
(previous, next) {
|
||||
|
|
@ -165,13 +150,17 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
|
|||
//Fixes small overlay problems with thumbnail
|
||||
Transform.scale(
|
||||
scaleY: 1.004,
|
||||
child: Video(
|
||||
fit: BoxFit.contain,
|
||||
fill: const Color.fromARGB(0, 123, 62, 62),
|
||||
controller: controller,
|
||||
controls: NoVideoControls,
|
||||
wakelock: false,
|
||||
child: player.videoWidget(
|
||||
UniqueKey(),
|
||||
BoxFit.contain,
|
||||
),
|
||||
// child: Video(
|
||||
// fit: BoxFit.contain,
|
||||
// fill: const Color.fromARGB(0, 123, 62, 62),
|
||||
// controller: controller,
|
||||
// controls: NoVideoControls,
|
||||
// wakelock: false,
|
||||
// ),
|
||||
),
|
||||
IgnorePointer(
|
||||
ignoring: !widget.showOverlay,
|
||||
|
|
@ -211,7 +200,7 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
|
|||
}
|
||||
},
|
||||
onChangeStart: (value) {
|
||||
wasPlaying = player.state.playing;
|
||||
wasPlaying = player.lastState.playing;
|
||||
player.pause();
|
||||
},
|
||||
onChanged: (e) {
|
||||
|
|
@ -239,7 +228,7 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
|
|||
player.playOrPause();
|
||||
},
|
||||
icon: Icon(
|
||||
player.state.playing ? IconsaxBold.pause_circle : IconsaxBold.play_circle,
|
||||
player.lastState.playing ? IconsaxBold.pause_circle : IconsaxBold.play_circle,
|
||||
shadows: [
|
||||
BoxShadow(blurRadius: 16, spreadRadius: 2, color: Colors.black.withOpacity(0.15))
|
||||
],
|
||||
|
|
|
|||
|
|
@ -82,34 +82,90 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
|||
),
|
||||
const Divider(),
|
||||
SettingsLabelDivider(label: context.localized.advanced),
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.settingsPlayerVideoHWAccelTitle),
|
||||
subLabel: Text(context.localized.settingsPlayerVideoHWAccelDesc),
|
||||
onTap: () => provider.setHardwareAccel(!videoSettings.hardwareAccel),
|
||||
trailing: Switch(
|
||||
value: videoSettings.hardwareAccel,
|
||||
onChanged: (value) => provider.setHardwareAccel(value),
|
||||
),
|
||||
),
|
||||
if (!kIsWeb) ...[
|
||||
if (PlayerOptions.available.length != 1)
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.settingsPlayerNativeLibassAccelTitle),
|
||||
subLabel: Text(context.localized.settingsPlayerNativeLibassAccelDesc),
|
||||
onTap: () => provider.setUseLibass(!videoSettings.useLibass),
|
||||
trailing: Switch(
|
||||
value: videoSettings.useLibass,
|
||||
onChanged: (value) => provider.setUseLibass(value),
|
||||
),
|
||||
),
|
||||
AnimatedFadeSize(
|
||||
child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid
|
||||
? SettingsMessageBox(
|
||||
context.localized.settingsPlayerMobileWarning,
|
||||
messageType: MessageType.warning,
|
||||
label: Text(context.localized.playerSettingsBackendTitle),
|
||||
subLabel: Text(context.localized.playerSettingsBackendDesc),
|
||||
trailing: Builder(builder: (context) {
|
||||
final wantedPlayer = ref.watch(videoPlayerSettingsProvider.select((value) => value.wantedPlayer));
|
||||
final currentPlayer = ref.watch(videoPlayerSettingsProvider.select((value) => value.playerOptions));
|
||||
return EnumBox(
|
||||
current: currentPlayer == null
|
||||
? "${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})"
|
||||
: wantedPlayer.label(context),
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: null,
|
||||
child:
|
||||
Text("${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})"),
|
||||
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
|
||||
ref.read(videoPlayerSettingsProvider).copyWith(playerOptions: null),
|
||||
),
|
||||
...PlayerOptions.available.map(
|
||||
(entry) => PopupMenuItem(
|
||||
value: entry,
|
||||
child: Text(entry.label(context)),
|
||||
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
|
||||
ref.read(videoPlayerSettingsProvider).copyWith(playerOptions: entry),
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
AnimatedFadeSize(
|
||||
child: switch (ref.read(videoPlayerSettingsProvider.select((value) => value.wantedPlayer))) {
|
||||
PlayerOptions.libMPV => Column(
|
||||
children: [
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.settingsPlayerVideoHWAccelTitle),
|
||||
subLabel: Text(context.localized.settingsPlayerVideoHWAccelDesc),
|
||||
onTap: () => provider.setHardwareAccel(!videoSettings.hardwareAccel),
|
||||
trailing: Switch(
|
||||
value: videoSettings.hardwareAccel,
|
||||
onChanged: (value) => provider.setHardwareAccel(value),
|
||||
),
|
||||
),
|
||||
if (!kIsWeb) ...[
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.settingsPlayerNativeLibassAccelTitle),
|
||||
subLabel: Text(context.localized.settingsPlayerNativeLibassAccelDesc),
|
||||
onTap: () => provider.setUseLibass(!videoSettings.useLibass),
|
||||
trailing: Switch(
|
||||
value: videoSettings.useLibass,
|
||||
onChanged: (value) => provider.setUseLibass(value),
|
||||
),
|
||||
),
|
||||
AnimatedFadeSize(
|
||||
child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid
|
||||
? SettingsMessageBox(
|
||||
context.localized.settingsPlayerMobileWarning,
|
||||
messageType: MessageType.warning,
|
||||
)
|
||||
: Container(),
|
||||
),
|
||||
],
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.settingsPlayerCustomSubtitlesTitle),
|
||||
subLabel: Text(context.localized.settingsPlayerCustomSubtitlesDesc),
|
||||
onTap: videoSettings.useLibass
|
||||
? null
|
||||
: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
useSafeArea: false,
|
||||
builder: (context) => const SubtitleEditor(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
_ => SettingsMessageBox(
|
||||
messageType: MessageType.info,
|
||||
"${context.localized.noVideoPlayerOptions}\n${context.localized.mdkExperimental}")
|
||||
},
|
||||
),
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.settingsAutoNextTitle),
|
||||
subLabel: Text(context.localized.settingsAutoNextDesc),
|
||||
|
|
@ -138,20 +194,6 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
|||
_ => const SizedBox.shrink(),
|
||||
},
|
||||
),
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.settingsPlayerCustomSubtitlesTitle),
|
||||
subLabel: Text(context.localized.settingsPlayerCustomSubtitlesDesc),
|
||||
onTap: videoSettings.useLibass
|
||||
? null
|
||||
: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
useSafeArea: false,
|
||||
builder: (context) => const SubtitleEditor(),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb)
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.playerSettingsOrientationTitle),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_blurhash/flutter_blurhash.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/settings/subtitle_settings_model.dart';
|
||||
import 'package:fladder/providers/settings/subtitle_settings_provider.dart';
|
||||
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||
|
|
@ -5,10 +10,6 @@ import 'package:fladder/screens/video_player/components/video_subtitle_controls.
|
|||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/fladder_appbar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_blurhash/flutter_blurhash.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
|
||||
class SubtitleEditor extends ConsumerStatefulWidget {
|
||||
const SubtitleEditor({super.key});
|
||||
|
|
|
|||
|
|
@ -14,35 +14,43 @@ class FlatButton extends ConsumerWidget {
|
|||
final Color? splashColor;
|
||||
final double elevation;
|
||||
final Clip clipBehavior;
|
||||
const FlatButton(
|
||||
{this.child,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.onDoubleTap,
|
||||
this.onSecondaryTapDown,
|
||||
this.borderRadiusGeometry,
|
||||
this.splashColor,
|
||||
this.elevation = 0,
|
||||
this.clipBehavior = Clip.none,
|
||||
super.key});
|
||||
const FlatButton({
|
||||
this.child,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.onDoubleTap,
|
||||
this.onSecondaryTapDown,
|
||||
this.borderRadiusGeometry,
|
||||
this.splashColor,
|
||||
this.elevation = 0,
|
||||
this.clipBehavior = Clip.none,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
clipBehavior: clipBehavior,
|
||||
borderRadius: borderRadiusGeometry ?? FladderTheme.defaultShape.borderRadius,
|
||||
elevation: 0,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
onDoubleTap: onDoubleTap,
|
||||
onSecondaryTapDown: onSecondaryTapDown,
|
||||
borderRadius: borderRadiusGeometry ?? BorderRadius.circular(10),
|
||||
splashColor: splashColor ?? Theme.of(context).colorScheme.primary.withOpacity(0.5),
|
||||
splashFactory: InkSparkle.splashFactory,
|
||||
child: child ?? Container(),
|
||||
),
|
||||
return Stack(
|
||||
fit: StackFit.passthrough,
|
||||
children: [
|
||||
child ?? Container(),
|
||||
Positioned.fill(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
clipBehavior: clipBehavior,
|
||||
borderRadius: borderRadiusGeometry ?? FladderTheme.defaultShape.borderRadius,
|
||||
elevation: 0,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
onDoubleTap: onDoubleTap,
|
||||
onSecondaryTapDown: onSecondaryTapDown,
|
||||
borderRadius: borderRadiusGeometry ?? BorderRadius.circular(10),
|
||||
splashColor: splashColor ?? Theme.of(context).colorScheme.primary.withOpacity(0.5),
|
||||
splashFactory: InkSparkle.splashFactory,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/playback/playback_model.dart';
|
||||
import 'package:fladder/providers/session_info_provider.dart';
|
||||
import 'package:fladder/providers/video_player_provider.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
|
||||
Future<void> showVideoPlaybackInformation(BuildContext context) {
|
||||
return showDialog(
|
||||
|
|
@ -19,6 +22,7 @@ class _VideoPlaybackInformation extends ConsumerWidget {
|
|||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final playbackModel = ref.watch(playBackModel);
|
||||
final sessionInfo = ref.watch(sessionInfoProvider);
|
||||
final backend = ref.read(videoPlayerProvider.select((value) => value.backend));
|
||||
return Dialog(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
|
|
@ -27,47 +31,81 @@ class _VideoPlaybackInformation extends ConsumerWidget {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Playback information", style: Theme.of(context).textTheme.titleMedium),
|
||||
Text("Player info", style: Theme.of(context).textTheme.titleMedium),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4).copyWith(top: 4),
|
||||
child: Opacity(
|
||||
opacity: 0.80,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [const Text('backend: '), Text(backend?.label(context) ?? context.localized.unknown)],
|
||||
)
|
||||
].addPadding(const EdgeInsets.symmetric(vertical: 3)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
...[
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [const Text('type: '), Text(playbackModel.label ?? "")],
|
||||
),
|
||||
if (sessionInfo.transCodeInfo != null) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text("Transcoding", style: Theme.of(context).textTheme.titleMedium),
|
||||
if (sessionInfo.transCodeInfo?.transcodeReasons?.isNotEmpty == true)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [const Text('reason: '), Text(sessionInfo.transCodeInfo?.transcodeReasons.toString() ?? "")],
|
||||
),
|
||||
if (sessionInfo.transCodeInfo?.completionPercentage != null)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('transcode progress: '),
|
||||
Text("${sessionInfo.transCodeInfo?.completionPercentage?.toStringAsFixed(2)} %")
|
||||
Text("Playback information", style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4).copyWith(top: 4),
|
||||
child: Opacity(
|
||||
opacity: 0.8,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [const Text('type: '), Text(playbackModel.label ?? "")],
|
||||
),
|
||||
if (sessionInfo.transCodeInfo != null) ...[
|
||||
Text("Transcoding", style: Theme.of(context).textTheme.titleMedium),
|
||||
if (sessionInfo.transCodeInfo?.transcodeReasons?.isNotEmpty == true)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('reason: '),
|
||||
Text(sessionInfo.transCodeInfo?.transcodeReasons.toString() ?? "")
|
||||
],
|
||||
),
|
||||
if (sessionInfo.transCodeInfo?.completionPercentage != null)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('transcode progress: '),
|
||||
Text("${sessionInfo.transCodeInfo?.completionPercentage?.toStringAsFixed(2)} %")
|
||||
],
|
||||
),
|
||||
if (sessionInfo.transCodeInfo?.container != null)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('container: '),
|
||||
Text(sessionInfo.transCodeInfo!.container.toString())
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (sessionInfo.transCodeInfo?.container != null)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [const Text('container: '), Text(sessionInfo.transCodeInfo!.container.toString())],
|
||||
),
|
||||
],
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [const Text('resolution: '), Text(playbackModel?.item.streamModel?.resolutionText ?? "")],
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('resolution: '),
|
||||
Text(playbackModel?.item.streamModel?.resolutionText ?? "")
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('container: '),
|
||||
Text(playbackModel?.playbackInfo?.mediaSources?.firstOrNull?.container ?? "")
|
||||
],
|
||||
)
|
||||
].addPadding(const EdgeInsets.symmetric(vertical: 3)),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('container: '),
|
||||
Text(playbackModel?.playbackInfo?.mediaSources?.firstOrNull?.container ?? "")
|
||||
],
|
||||
),
|
||||
].addPadding(const EdgeInsets.symmetric(vertical: 3))
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
||||
import 'package:fladder/providers/video_player_provider.dart';
|
||||
import 'package:fladder/screens/video_player/components/video_player_chapters.dart';
|
||||
|
|
@ -9,8 +8,7 @@ import 'package:fladder/screens/video_player/components/video_player_queue.dart'
|
|||
|
||||
class ChapterButton extends ConsumerWidget {
|
||||
final Duration position;
|
||||
final Player player;
|
||||
const ChapterButton({super.key, required this.position, required this.player});
|
||||
const ChapterButton({super.key, required this.position});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
|
|
@ -22,9 +20,9 @@ class ChapterButton extends ConsumerWidget {
|
|||
context,
|
||||
chapters: currentChapters,
|
||||
currentPosition: position,
|
||||
onChapterTapped: (chapter) => player.seek(
|
||||
chapter.startPosition,
|
||||
),
|
||||
onChapterTapped: (chapter) => ref.read(videoPlayerProvider).seek(
|
||||
chapter.startPosition,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
|
|
|
|||
|
|
@ -447,13 +447,13 @@ class _SimpleControls extends ConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final player = ref.watch(videoPlayerProvider.select((value) => value.controller?.player));
|
||||
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(),
|
||||
onPressed: () => player.playOrPause(),
|
||||
icon: Icon(isPlaying ? IconsaxBold.pause : IconsaxBold.play),
|
||||
),
|
||||
if (skip != null)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import 'package:fladder/models/playback/direct_playback_model.dart';
|
|||
import 'package:fladder/models/playback/offline_playback_model.dart';
|
||||
import 'package:fladder/models/playback/playback_model.dart';
|
||||
import 'package:fladder/models/playback/transcode_playback_model.dart';
|
||||
import 'package:fladder/models/settings/video_player_settings.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';
|
||||
|
|
@ -375,15 +376,16 @@ Future<void> showSubSelection(BuildContext context) {
|
|||
children: [
|
||||
Text(context.localized.subtitle),
|
||||
const Spacer(),
|
||||
IconButton.outlined(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
showSubtitleControls(
|
||||
context: context,
|
||||
label: context.localized.subtitleConfiguration,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.display_settings_rounded))
|
||||
if (player.backend == PlayerOptions.libMPV)
|
||||
IconButton.outlined(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
showSubtitleControls(
|
||||
context: context,
|
||||
label: context.localized.subtitleConfiguration,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.display_settings_rounded))
|
||||
],
|
||||
),
|
||||
children: playbackModel?.subStreams?.mapIndexed(
|
||||
|
|
@ -459,7 +461,7 @@ Future<void> showPlaybackSpeed(BuildContext context) {
|
|||
return StatefulBuilder(builder: (context, setState) {
|
||||
return Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final player = ref.watch(videoPlayerProvider.select((value) => value.player));
|
||||
final player = ref.watch(videoPlayerProvider);
|
||||
final lastSpeed = ref.watch(playbackRateProvider);
|
||||
return SimpleDialog(
|
||||
contentPadding: const EdgeInsets.only(top: 8, bottom: 24),
|
||||
|
|
@ -484,7 +486,7 @@ Future<void> showPlaybackSpeed(BuildContext context) {
|
|||
divisions: 39,
|
||||
onChanged: (value) {
|
||||
ref.read(playbackRateProvider.notifier).state = value;
|
||||
player?.setRate(value);
|
||||
player.setSpeed(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ class _ChapterProgressSliderState extends ConsumerState<VideoProgressBar> {
|
|||
setState(() {
|
||||
onHoverStart = true;
|
||||
});
|
||||
widget.wasPlayingChanged.call(player.player?.state.playing ?? false);
|
||||
widget.wasPlayingChanged.call(player.lastState?.playing ?? false);
|
||||
player.pause();
|
||||
},
|
||||
onChanged: (e) {
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fladder/providers/settings/subtitle_settings_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
|
||||
import 'package:fladder/models/settings/subtitle_settings_model.dart';
|
||||
|
||||
class VideoSubtitles extends ConsumerStatefulWidget {
|
||||
final VideoController controller;
|
||||
final bool overLayed;
|
||||
const VideoSubtitles({
|
||||
required this.controller,
|
||||
this.overLayed = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _VideoSubtitlesState();
|
||||
}
|
||||
|
||||
class _VideoSubtitlesState extends ConsumerState<VideoSubtitles> {
|
||||
late List<String> subtitle = widget.controller.player.state.subtitle;
|
||||
StreamSubscription<List<String>>? subscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
subscription = widget.controller.player.stream.subtitle.listen((value) {
|
||||
setState(() {
|
||||
subtitle = value;
|
||||
});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
subscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final settings = ref.watch(subtitleSettingsProvider);
|
||||
final padding = MediaQuery.of(context).padding;
|
||||
final text = [
|
||||
for (final line in subtitle)
|
||||
if (line.trim().isNotEmpty) line.trim(),
|
||||
].join('\n');
|
||||
|
||||
if (widget.controller.player.platform?.configuration.libass ?? false) {
|
||||
return const IgnorePointer(child: SizedBox());
|
||||
} else {
|
||||
return SubtitleText(
|
||||
subModel: settings,
|
||||
padding: padding,
|
||||
offset: (widget.overLayed ? 0.5 : settings.verticalOffset),
|
||||
text: text,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
|
||||
import 'package:fladder/models/media_playback_model.dart';
|
||||
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||
|
|
@ -73,7 +72,7 @@ class _VideoPlayerState extends ConsumerState<VideoPlayer> with WidgetsBindingOb
|
|||
final videoFit = ref.watch(videoPlayerSettingsProvider.select((value) => value.videoFit));
|
||||
final padding = MediaQuery.of(context).padding;
|
||||
|
||||
final playerController = ref.watch(videoPlayerProvider.select((value) => value.controller));
|
||||
final playerController = ref.watch(videoPlayerProvider.select((value) => value));
|
||||
|
||||
ref.listen(
|
||||
videoPlayerSettingsProvider.select((value) => value.allowedOrientations),
|
||||
|
|
@ -103,24 +102,15 @@ class _VideoPlayerState extends ConsumerState<VideoPlayer> with WidgetsBindingOb
|
|||
lastScale = 0.0;
|
||||
},
|
||||
child: VideoPlayerNextWrapper(
|
||||
video: playerController != null
|
||||
? Padding(
|
||||
padding: fillScreen ? EdgeInsets.zero : EdgeInsets.only(left: padding.left, right: padding.right),
|
||||
child: OrientationBuilder(builder: (context, orientation) {
|
||||
return Video(
|
||||
key: Key(orientation.toString()),
|
||||
controller: playerController,
|
||||
fill: Colors.transparent,
|
||||
wakelock: true,
|
||||
fit: fillScreen
|
||||
? (MediaQuery.of(context).orientation == Orientation.portrait ? videoFit : BoxFit.cover)
|
||||
: videoFit,
|
||||
subtitleViewConfiguration: const SubtitleViewConfiguration(visible: false),
|
||||
controls: NoVideoControls,
|
||||
);
|
||||
}),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
video: Padding(
|
||||
padding: fillScreen ? EdgeInsets.zero : EdgeInsets.only(left: padding.left, right: padding.right),
|
||||
child: playerController.videoWidget(
|
||||
const Key("VideoPlayer"),
|
||||
fillScreen
|
||||
? (MediaQuery.of(context).orientation == Orientation.portrait ? videoFit : BoxFit.cover)
|
||||
: videoFit,
|
||||
),
|
||||
),
|
||||
controls: const DesktopControls(),
|
||||
overlays: [
|
||||
if (errorPlaying) const _VideoErrorWidget(),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
|
@ -23,7 +22,6 @@ import 'package:fladder/screens/video_player/components/video_player_controls_ex
|
|||
import 'package:fladder/screens/video_player/components/video_player_options_sheet.dart';
|
||||
import 'package:fladder/screens/video_player/components/video_player_seek_indicator.dart';
|
||||
import 'package:fladder/screens/video_player/components/video_progress_bar.dart';
|
||||
import 'package:fladder/screens/video_player/components/video_subtitles.dart';
|
||||
import 'package:fladder/screens/video_player/components/video_volume_slider.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/duration_extensions.dart';
|
||||
|
|
@ -31,8 +29,7 @@ import 'package:fladder/util/input_handler.dart';
|
|||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/string_extensions.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/full_screen_button.dart';
|
||||
|
||||
class DesktopControls extends ConsumerStatefulWidget {
|
||||
const DesktopControls({super.key});
|
||||
|
|
@ -115,7 +112,8 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mediaSegments = ref.watch(playBackModel.select((value) => value?.mediaSegments));
|
||||
final player = ref.watch(videoPlayerProvider.select((value) => value.controller));
|
||||
final player = ref.watch(videoPlayerProvider);
|
||||
final subtitleWidget = player.subtitleWidget(showOverlay);
|
||||
return InputHandler(
|
||||
autoFocus: false,
|
||||
onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
|
||||
|
|
@ -135,12 +133,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
|||
onHover: AdaptiveLayout.of(context).isDesktop ? (event) => toggleOverlay(value: true) : null,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (player != null)
|
||||
VideoSubtitles(
|
||||
key: const Key('subtitles'),
|
||||
controller: player,
|
||||
overLayed: showOverlay,
|
||||
),
|
||||
if (subtitleWidget != null) subtitleWidget,
|
||||
if (AdaptiveLayout.of(context).isDesktop)
|
||||
Consumer(builder: (context, ref, child) {
|
||||
final playing = ref.watch(mediaPlaybackProvider.select((value) => value.playing));
|
||||
|
|
@ -385,7 +378,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
|||
onPressed: () => closePlayer(), icon: const Icon(IconsaxOutline.close_square))),
|
||||
const Spacer(),
|
||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer &&
|
||||
ref.read(videoPlayerProvider).player != null) ...{
|
||||
ref.read(videoPlayerProvider).hasPlayer) ...{
|
||||
// OpenQueueButton(x),
|
||||
// ChapterButton(
|
||||
// position: position,
|
||||
|
|
@ -471,7 +464,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
|||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
},
|
||||
].addPadding(const EdgeInsets.symmetric(horizontal: 4)),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue