Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:flutter_vodozemac/flutter_vodozemac.dart' as vod;
import 'package:matrix/matrix.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:timezone/data/latest.dart' as tz;
import 'package:media_kit/media_kit.dart';

import 'package:extera_next/config/app_config.dart';
import 'package:extera_next/utils/client_manager.dart';
Expand Down Expand Up @@ -40,6 +41,8 @@ void main() async {
// widget bindings are initialized already.
WidgetsFlutterBinding.ensureInitialized();

MediaKit.ensureInitialized();

tz.initializeTimeZones();

FlutterForegroundTask.initCommunicationPort();
Expand Down
2 changes: 1 addition & 1 deletion lib/pages/image_viewer/image_viewer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class ImageViewerController extends State<ImageViewer> {
(event) => {
MessageTypes.Image,
MessageTypes.Sticker,
if (PlatformInfos.supportsVideoPlayer) MessageTypes.Video,
MessageTypes.Video,
}.contains(event.messageType),
)
.toList()
Expand Down
73 changes: 69 additions & 4 deletions lib/pages/image_viewer/image_viewer_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import 'package:matrix/matrix.dart';

import 'package:extera_next/generated/l10n/l10n.dart';
import 'package:extera_next/pages/image_viewer/video_player.dart';
import 'package:extera_next/utils/clipboard_utils.dart';
import 'package:extera_next/utils/matrix_sdk_extensions/event_extension.dart';
import 'package:extera_next/utils/platform_infos.dart';
import 'package:extera_next/widgets/hover_builder.dart';
import 'package:extera_next/widgets/mxc_image.dart';
Expand Down Expand Up @@ -195,21 +197,83 @@ class _ZoomableImageState extends State<_ZoomableImage> {
super.dispose();
}

void _onSecondaryTapUp(TapUpDetails details) {
if (!PlatformInfos.isDesktop) return;
final overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
showMenu<int>(
context: context,
position: RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
overlay.size.width - details.globalPosition.dx,
overlay.size.height - details.globalPosition.dy,
),
items: [
PopupMenuItem(
value: 0,
child: Row(
children: [
const Icon(Icons.copy_outlined, size: 20),
const SizedBox(width: 8),
Text(L10n.of(context).copy),
],
),
),
PopupMenuItem(
value: 1,
child: Row(
children: [
const Icon(Icons.save_alt_outlined, size: 20),
const SizedBox(width: 8),
Text(L10n.of(context).downloadFile),
],
),
),
],
).then((value) {
if (value == 0) {
_copyImageToClipboard();
} else if (value == 1) {
widget.event.saveFile(context);
}
});
}

Future<void> _copyImageToClipboard() async {
try {
final data = await widget.event.downloadAndDecryptAttachment(
getThumbnail: false,
);
await writeImageToClipboard(data.bytes);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context).copiedToClipboard),
duration: const Duration(seconds: 1),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to copy image: $e')),
);
}
}
}

@override
Widget build(BuildContext context) {
return InteractiveViewer(
transformationController: _transformController,
minScale: 1.0,
maxScale: 10.0,
// When the user puts fingers on screen, disable PageView scroll
onInteractionStart: (_) {
widget.onZoomStatusChanged(true);
},
// When interaction ends, check if we are still zoomed in
onInteractionEnd: (details) {
widget.controller.onInteractionEnds(details);

// Identity matrix means scale is 1.0 and offset is 0,0
final isZoomed = _transformController.value.row0[0] != 1.0;
if (!isZoomed) {
widget.onZoomStatusChanged(false);
Expand All @@ -220,6 +284,7 @@ class _ZoomableImageState extends State<_ZoomableImage> {
tag: widget.event.eventId,
child: GestureDetector(
onTap: () {},
onSecondaryTapUp: _onSecondaryTapUp,
child: MxcImage(
key: ValueKey(widget.event.eventId),
event: widget.event,
Expand Down
184 changes: 83 additions & 101 deletions lib/pages/image_viewer/video_player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import 'package:chewie/chewie.dart';
import 'package:matrix/matrix.dart';
import 'package:path_provider/path_provider.dart';
import 'package:universal_html/html.dart' as html;
import 'package:video_player/video_player.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';

import 'package:extera_next/pages/image_viewer/image_viewer.dart';
import 'package:extera_next/utils/localized_exception_extension.dart';
import 'package:extera_next/utils/matrix_sdk_extensions/event_extension.dart';
import 'package:extera_next/utils/platform_infos.dart';
import 'package:extera_next/widgets/blur_hash.dart';
import '../../../utils/error_reporter.dart';
Expand All @@ -28,25 +26,17 @@ class EventVideoPlayer extends StatefulWidget {
}

class EventVideoPlayerState extends State<EventVideoPlayer> {
ChewieController? _chewieController;
VideoPlayerController? _videoPlayerController;
Player? _mediaKitPlayer;
VideoController? _mediaKitController;

double? _downloadProgress;

// The video_player package only doesn't support Windows and Linux.
final _supportsVideoPlayer =
!PlatformInfos.isWindows && !PlatformInfos.isLinux;

void _downloadAction() async {
if (!_supportsVideoPlayer) {
widget.event.saveFile(context);
return;
}

try {
// Dispose the controllers if we already have them.
_disposeControllers();
late VideoPlayerController videoPlayerController;
final player = Player();
_mediaKitPlayer = player;
_mediaKitController = VideoController(player);

if (widget.event.room.encrypted) {
final fileSize = widget.event.content
Expand All @@ -65,68 +55,54 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
});
},
);
// Create the VideoPlayerController from the contents of videoFile.
if (kIsWeb) {
final blob = html.Blob([videoFile.bytes]);
final networkUri = Uri.parse(html.Url.createObjectUrlFromBlob(blob));
videoPlayerController = VideoPlayerController.networkUrl(networkUri);
} else {
final tempDir = await getTemporaryDirectory();
final fileName = Uri.encodeComponent(
widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last,
);
final file = File('${tempDir.path}/${fileName}_${videoFile.name}');
if (await file.exists() == false) {
await file.writeAsBytes(videoFile.bytes);
}
videoPlayerController = VideoPlayerController.file(file);

final tempDir = await getTemporaryDirectory();
final fileName = Uri.encodeComponent(
widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last,
);
final file = File('${tempDir.path}/${fileName}_${videoFile.name}');
if (!await file.exists()) {
await file.writeAsBytes(videoFile.bytes);
}
await player.open(Media(file.path));
} else {
final videoUrl = await widget.event.attachmentMxcUrl!.getDownloadUri(
widget.event.room.client,
);
Logs().d("Video url: $videoUrl");
videoPlayerController = VideoPlayerController.networkUrl(
videoUrl,
httpHeaders: {
'authorization': 'Bearer ${widget.event.room.client.accessToken}',
},
await player.open(
Media(
videoUrl.toString(),
httpHeaders: {
'authorization': 'Bearer ${widget.event.room.client.accessToken}',
},
),
);
}

_videoPlayerController = videoPlayerController;

await videoPlayerController.initialize();

if (widget.ivController.currentEvent.eventId != widget.event.eventId) {
dispose();
return;
}

// Create a ChewieController on top.
setState(() {
_chewieController = ChewieController(
videoPlayerController: videoPlayerController,
useRootNavigator: !kIsWeb,
autoPlay: true,
autoInitialize: true,
looping: true,
);
});
setState(() {});
} on IOException catch (e) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
}
} catch (e, s) {
ErrorReporter(context, 'Unable to play video').onErrorCallback(e, s);
if (mounted) {
ErrorReporter(context, 'Unable to play video').onErrorCallback(e, s);
}
}
}

void _disposeControllers() {
_chewieController?.dispose();
_videoPlayerController?.dispose();
_chewieController = null;
_videoPlayerController = null;
_mediaKitPlayer?.dispose();
_mediaKitPlayer = null;
_mediaKitController = null;
}

@override
Expand Down Expand Up @@ -159,47 +135,53 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
final height = MediaQuery.of(context).size.height - 52;
final width = videoWidth * (height / videoHeight);

final chewieController = _chewieController;
return chewieController != null
? Center(
child: SizedBox(
width: width,
height: height,
child: Chewie(controller: chewieController),
),
)
: Stack(
children: [
Center(
child: Hero(
tag: widget.event.eventId,
child: hasThumbnail
? MxcImage(
event: widget.event,
isThumbnail: true,
width: width,
height: height,
fit: BoxFit.cover,
placeholder: (context) => BlurHash(
blurhash: blurHash,
width: width,
height: height,
fit: BoxFit.cover,
),
)
: BlurHash(
blurhash: blurHash,
width: width,
height: height,
),
),
),
Center(
child: CircularProgressIndicator.adaptive(
value: _downloadProgress,
),
),
],
);
final mediaKitController = _mediaKitController;

if (mediaKitController != null) {
return Center(
child: SizedBox(
width: width,
height: height,
child: Video(
controller: mediaKitController,
controls: MaterialVideoControls,
),
),
);
}

return Stack(
children: [
Center(
child: Hero(
tag: widget.event.eventId,
child: hasThumbnail
? MxcImage(
event: widget.event,
isThumbnail: true,
width: width,
height: height,
fit: BoxFit.cover,
placeholder: (context) => BlurHash(
blurhash: blurHash,
width: width,
height: height,
fit: BoxFit.cover,
),
)
: BlurHash(
blurhash: blurHash,
width: width,
height: height,
),
),
),
Center(
child: CircularProgressIndicator.adaptive(
value: _downloadProgress,
),
),
],
);
}
}
Loading