forked from Nexus/nexus
135 lines
5 KiB
Dart
135 lines
5 KiB
Dart
import "dart:async";
|
|
|
|
import "package:material_ui/material_ui.dart";
|
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
|
import "package:nexus/controllers/client.dart";
|
|
import "package:nexus/helpers/extensions/save_attachment.dart";
|
|
import "package:nexus/models/info/video.dart";
|
|
import "package:flutter_hooks/flutter_hooks.dart";
|
|
import "package:media_kit/media_kit.dart";
|
|
import "package:media_kit_video/media_kit_video.dart";
|
|
import "package:nexus/widgets/players/player_controls.dart";
|
|
|
|
class const VideoPlayer(
|
|
final Uri uri,
|
|
final VideoInfo? info, {
|
|
final bool encrypted = false,
|
|
final String? filename,
|
|
super.key,
|
|
}) extends HookConsumerWidget {
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final player = useMemoized(
|
|
() => Player(configuration: .new(bufferSize: 128 * 1024 * 1024)),
|
|
);
|
|
final controller = useMemoized(() => VideoController(player));
|
|
final loaded = useState(false);
|
|
|
|
useEffect(() {
|
|
scheduleMicrotask(() async {
|
|
final video = await ref
|
|
.read(ClientController.provider.notifier)
|
|
.downloadMedia(.new(mxc: uri, encrypted: encrypted));
|
|
await player.open(Media(video.path), play: false);
|
|
if (context.mounted) loaded.value = true;
|
|
});
|
|
|
|
return player.dispose;
|
|
}, []);
|
|
|
|
final aspectRatio = switch (info) {
|
|
VideoInfo(:final width?, :final height?) when width > 0 && height > 0 =>
|
|
width / height,
|
|
_ => 16 / 9,
|
|
};
|
|
|
|
return ConstrainedBox(
|
|
constraints: const .new(maxHeight: 300),
|
|
child: AspectRatio(
|
|
aspectRatio: aspectRatio,
|
|
child: ClipRRect(
|
|
borderRadius: .circular(12),
|
|
child: Video(
|
|
controller: controller,
|
|
controls: (_) =>
|
|
// Until media_kit moves to material_ui, this Material is needed.
|
|
Material(
|
|
type: .transparency,
|
|
child: Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: GestureDetector(
|
|
behavior: .opaque,
|
|
onTap: loaded.value ? player.playOrPause : null,
|
|
),
|
|
),
|
|
Positioned(
|
|
top: 4,
|
|
right: 4,
|
|
child: IconButton(
|
|
onPressed: () => context.saveAttachment(
|
|
ref,
|
|
uri,
|
|
encrypted: encrypted,
|
|
filename: filename,
|
|
),
|
|
color: Colors.white,
|
|
style: IconButton.styleFrom(
|
|
backgroundColor: Colors.black38,
|
|
),
|
|
tooltip: "Download",
|
|
icon: Icon(Icons.download),
|
|
),
|
|
),
|
|
Positioned(
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
child: DecoratedBox(
|
|
decoration: const BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: .bottomCenter,
|
|
end: .topCenter,
|
|
colors: [Colors.black54, Colors.transparent],
|
|
),
|
|
),
|
|
child: Padding(
|
|
padding: const .only(left: 4, right: 4, top: 16),
|
|
child: PlayerControls(
|
|
player,
|
|
loading: !loaded.value,
|
|
color: Colors.white,
|
|
actions: .new([
|
|
Builder(
|
|
builder: (context) {
|
|
final fullscreen = isFullscreen(context);
|
|
|
|
return IconButton(
|
|
onPressed: () =>
|
|
toggleFullscreen(context),
|
|
color: Colors.white,
|
|
tooltip: fullscreen
|
|
? "Exit fullscreen"
|
|
: "Fullscreen",
|
|
icon: Icon(
|
|
fullscreen
|
|
? Icons.fullscreen_exit
|
|
: Icons.fullscreen,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
]),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|