This commit is contained in:
otsmr 2025-05-21 23:30:06 +02:00
parent 7a300456a3
commit cedf20500c
13 changed files with 574 additions and 87 deletions

View file

@ -81,6 +81,13 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
.get(); .get();
} }
Future<List<Message>> getAllStoredMediaFiles() {
return (select(messages)
..where((t) => t.mediaStored.equals(true))
..orderBy([(t) => OrderingTerm.desc(t.sendAt)]))
.get();
}
Future<List<Message>> getAllMessagesPendingUploadOlderThanAMinute() { Future<List<Message>> getAllMessagesPendingUploadOlderThanAMinute() {
return (select(messages) return (select(messages)
..where( ..where(

View file

@ -53,7 +53,7 @@ Future sendMediaFile(
Uint8List imageBytes, Uint8List imageBytes,
bool isRealTwonly, bool isRealTwonly,
int maxShowTime, int maxShowTime,
XFile? videoFilePath, File? videoFilePath,
bool? enableVideoAudio, bool? enableVideoAudio,
bool mirrorVideo, bool mirrorVideo,
) async { ) async {
@ -75,7 +75,7 @@ Future sendMediaFile(
if (mediaUploadId != null) { if (mediaUploadId != null) {
if (videoFilePath != null) { if (videoFilePath != null) {
String basePath = await getMediaFilePath(mediaUploadId, "send"); String basePath = await getMediaFilePath(mediaUploadId, "send");
await File(videoFilePath.path).rename("$basePath.orginal.mp4"); await videoFilePath.rename("$basePath.orginal.mp4");
} }
await writeMediaFile(mediaUploadId, "orginal.png", imageBytes); await writeMediaFile(mediaUploadId, "orginal.png", imageBytes);
await handleSingleMediaFile(mediaUploadId, imageBytes); await handleSingleMediaFile(mediaUploadId, imageBytes);

View file

@ -72,6 +72,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
useHighQuality = user.useHighQuality!; useHighQuality = user.useHighQuality!;
} }
hasAudioPermission = await Permission.microphone.isGranted; hasAudioPermission = await Permission.microphone.isGranted;
if (!mounted) return;
setState(() {}); setState(() {});
} }
@ -155,6 +156,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
} }
} }
}); });
if (!mounted) {
return;
}
setState(() { setState(() {
cameraId = sCameraId; cameraId = sCameraId;
}); });
@ -239,7 +243,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
} }
Future<bool> pushMediaEditor( Future<bool> pushMediaEditor(
Future<Uint8List?>? imageBytes, XFile? videoFilePath) async { Future<Uint8List?>? imageBytes, File? videoFilePath) async {
bool? shoudReturn = await Navigator.push( bool? shoudReturn = await Navigator.push(
context, context,
PageRouteBuilder( PageRouteBuilder(
@ -361,16 +365,19 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
isVideoRecording = false; isVideoRecording = false;
sharePreviewIsShown = true; sharePreviewIsShown = true;
}); });
File? videoPathFile;
XFile? videoPath = await controller?.stopVideoRecording(); XFile? videoPath = await controller?.stopVideoRecording();
if (videoPath != null) { if (videoPath != null) {
if (Platform.isAndroid) { if (Platform.isAndroid) {
// see https://github.com/flutter/flutter/issues/148335 // see https://github.com/flutter/flutter/issues/148335
await File(videoPath.path).rename("${videoPath.path}.mp4"); await File(videoPath.path).rename("${videoPath.path}.mp4");
videoPath = XFile("${videoPath.path}.mp4"); videoPathFile = File("${videoPath.path}.mp4");
} else {
videoPathFile = File(videoPath.path);
} }
} }
await controller?.pausePreview(); await controller?.pausePreview();
if (await pushMediaEditor(null, videoPath)) { if (await pushMediaEditor(null, videoPathFile)) {
return; return;
} }
} on CameraException catch (e) { } on CameraException catch (e) {

View file

@ -1,4 +1,4 @@
import 'package:camera/camera.dart'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'dart:typed_data'; import 'dart:typed_data';
@ -8,7 +8,7 @@ import 'package:twonly/src/utils/misc.dart';
class SaveToGalleryButton extends StatefulWidget { class SaveToGalleryButton extends StatefulWidget {
final Future<Uint8List?> Function() getMergedImage; final Future<Uint8List?> Function() getMergedImage;
final String? sendNextMediaToUserName; final String? sendNextMediaToUserName;
final XFile? videoFilePath; final File? videoFilePath;
const SaveToGalleryButton( const SaveToGalleryButton(
{super.key, {super.key,

View file

@ -1,7 +1,6 @@
import 'dart:collection'; import 'dart:collection';
import 'dart:io'; import 'dart:io';
import 'dart:async'; import 'dart:async';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@ -43,7 +42,7 @@ class ShareImageEditorView extends StatefulWidget {
required this.useHighQuality, required this.useHighQuality,
}); });
final Future<Uint8List?>? imageBytes; final Future<Uint8List?>? imageBytes;
final XFile? videoFilePath; final File? videoFilePath;
final Contact? sendTo; final Contact? sendTo;
final bool mirrorVideo; final bool mirrorVideo;
final bool useHighQuality; final bool useHighQuality;
@ -77,8 +76,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
setState(() { setState(() {
sendingOrLoadingImage = false; sendingOrLoadingImage = false;
}); });
videoController = videoController = VideoPlayerController.file(widget.videoFilePath!);
VideoPlayerController.file(File(widget.videoFilePath!.path));
videoController?.setLooping(true); videoController?.setLooping(true);
videoController?.initialize().then((_) { videoController?.initialize().then((_) {
videoController!.play(); videoController!.play();

View file

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
@ -32,7 +32,7 @@ class ShareImageView extends StatefulWidget {
final bool isRealTwonly; final bool isRealTwonly;
final bool mirrorVideo; final bool mirrorVideo;
final int maxShowTime; final int maxShowTime;
final XFile? videoFilePath; final File? videoFilePath;
final HashSet<int> selectedUserIds; final HashSet<int> selectedUserIds;
final bool? enableVideoAudio; final bool? enableVideoAudio;
final Function(int, bool) updateStatus; final Function(int, bool) updateStatus;

View file

@ -7,7 +7,6 @@ import 'package:twonly/src/views/chats/chat_item_details_view.dart';
import 'package:twonly/src/views/chats/components/in_chat_media_viewer.dart'; import 'package:twonly/src/views/chats/components/in_chat_media_viewer.dart';
import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/animate_icon.dart';
import 'package:twonly/src/views/components/better_text.dart'; import 'package:twonly/src/views/components/better_text.dart';
import 'package:twonly/src/views/components/message_send_state_icon.dart';
import 'package:twonly/src/views/chats/components/sliding_response.dart'; import 'package:twonly/src/views/chats/components/sliding_response.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/tables/messages_table.dart';
@ -251,30 +250,12 @@ class ChatListEntry extends StatelessWidget {
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: InChatMediaViewer(message: message), child: InChatMediaViewer(
message: message,
contact: contact,
), ),
), ),
), ),
);
} else if (message.kind == MessageKind.storedMediaFile) {
child = Container(
padding: EdgeInsets.all(5),
width: 150,
decoration: BoxDecoration(
border: Border.all(
color:
getMessageColorFromType(TextMessageContent(text: ""), context),
width: 1.0,
),
borderRadius: BorderRadius.circular(12.0),
),
child: Align(
alignment: Alignment.centerRight,
child: MessageSendStateIcon(
[message],
mainAxisAlignment:
right ? MainAxisAlignment.center : MainAxisAlignment.center,
),
), ),
); );
} }

View file

@ -6,6 +6,9 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/providers/api/media_send.dart' as send; import 'package:twonly/src/providers/api/media_send.dart' as send;
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/components/media_view_sizing.dart';
import 'package:twonly/src/views/components/message_send_state_icon.dart'; import 'package:twonly/src/views/components/message_send_state_icon.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/tables/messages_table.dart';
@ -13,26 +16,107 @@ import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/providers/api/media_received.dart' as received; import 'package:twonly/src/providers/api/media_received.dart' as received;
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
class ChatMediaViewerFullScreen extends StatelessWidget { class ChatMediaViewerFullScreen extends StatefulWidget {
const ChatMediaViewerFullScreen({super.key, required this.message}); const ChatMediaViewerFullScreen(
{super.key, required this.message, required this.contact});
final Message message; final Message message;
final Contact contact;
@override
State<ChatMediaViewerFullScreen> createState() =>
_ChatMediaViewerFullScreenState();
}
class _ChatMediaViewerFullScreenState extends State<ChatMediaViewerFullScreen> {
bool hideMediaFile = false;
Future deleteFiles(context) async {
bool confirmed = await showAlertDialog(
context, "Are you sure?", "The image will be irrevocably deleted.");
if (!confirmed) return;
await twonlyDatabase.messagesDao.updateMessageByMessageId(
widget.message.messageId,
MessagesCompanion(mediaStored: Value(false)),
);
await send.purgeSendMediaFiles();
await received.purgeReceivedMediaFiles();
if (context.mounted) {
Navigator.pop(context, true);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: SafeArea( body: Container(
child: Center( child: MediaViewSizing(
child: InChatMediaViewer(message: message, isInFullscreen: true), bottomNavigation: Positioned(
bottom: 10,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton.outlined(
onPressed: () {
deleteFiles(context);
},
icon: FaIcon(FontAwesomeIcons.trashCan),
style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 20),
), ),
), ),
),
IconButton.filled(
icon: FaIcon(FontAwesomeIcons.camera),
onPressed: () async {
setState(() {
hideMediaFile = true;
});
await Navigator.push(context, MaterialPageRoute(
builder: (context) {
return CameraSendToView(widget.contact);
},
));
setState(() {
hideMediaFile = false;
});
},
style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 30),
),
backgroundColor: WidgetStateProperty.all<Color>(
Theme.of(context).colorScheme.primary,
)),
),
],
),
),
child: (hideMediaFile)
? Container()
: InChatMediaViewer(
message: widget.message,
contact: widget.contact,
isInFullscreen: true,
),
)),
); );
} }
} }
class InChatMediaViewer extends StatefulWidget { class InChatMediaViewer extends StatefulWidget {
const InChatMediaViewer( const InChatMediaViewer(
{super.key, required this.message, this.isInFullscreen = false}); {super.key,
required this.message,
required this.contact,
this.isInFullscreen = false});
final Message message; final Message message;
final Contact contact;
final bool isInFullscreen; final bool isInFullscreen;
@override @override
@ -78,6 +162,7 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
videoController!.setVolume(0); videoController!.setVolume(0);
} }
videoController!.play(); videoController!.play();
videoController!.setLooping(true);
}); });
setState(() { setState(() {
@ -100,29 +185,14 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
videoController?.dispose(); videoController?.dispose();
} }
Future deleteFiles() async { Future onTap() async {
await twonlyDatabase.messagesDao.updateMessageByMessageId( if (image == null && videoController == null) return;
widget.message.messageId,
MessagesCompanion(mediaStored: Value(false)),
);
await send.purgeSendMediaFiles();
await received.purgeReceivedMediaFiles();
if (context.mounted) {
Navigator.pop(context, true);
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: (image == null && videoController == null)
? null
: () async {
if (widget.isInFullscreen) return; if (widget.isInFullscreen) return;
bool? removed = await Navigator.push( bool? removed = await Navigator.push(
context, context,
MaterialPageRoute(builder: (context) { MaterialPageRoute(builder: (context) {
return ChatMediaViewerFullScreen(message: widget.message); return ChatMediaViewerFullScreen(
message: widget.message, contact: widget.contact);
}), }),
); );
@ -130,9 +200,14 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
image = null; image = null;
videoController?.dispose(); videoController?.dispose();
videoController = null; videoController = null;
setState(() {}); if (isMounted) setState(() {});
} }
}, }
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Stack( child: Stack(
children: [ children: [
if (image != null) Image.file(image!), if (image != null) Image.file(image!),
@ -151,22 +226,6 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
), ),
), ),
if (widget.isInFullscreen)
Positioned(
bottom: 10,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
OutlinedButton.icon(
onPressed: deleteFiles,
icon: FaIcon(FontAwesomeIcons.trashCan),
label: Text("Delete media file"),
)
],
),
),
], ],
), ),
); );

View file

@ -0,0 +1,46 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
class VideoPlayerWrapper extends StatefulWidget {
final File videoPath;
const VideoPlayerWrapper({super.key, required this.videoPath});
@override
State<VideoPlayerWrapper> createState() => _VideoPlayerWrapperState();
}
class _VideoPlayerWrapperState extends State<VideoPlayerWrapper> {
late VideoPlayerController _controller;
@override
void initState() {
super.initState();
_controller = VideoPlayerController.file(widget.videoPath)
..initialize().then((_) {
setState(() {
_controller.play(); // Auto-play the video
});
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: _controller.value.isInitialized
? AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
)
: CircularProgressIndicator(), // Show loading indicator while initializing
);
}
}

View file

@ -0,0 +1,374 @@
import 'dart:io';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart';
import 'package:twonly/src/providers/api/media_send.dart' as send;
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/share_image_editor_view.dart';
import 'package:twonly/src/views/components/media_view_sizing.dart';
import 'package:twonly/src/views/components/video_player_wrapper.dart';
import 'package:video_player/video_player.dart';
class GalleryItem {
GalleryItem({
required this.id,
required this.messages,
required this.date,
this.imagePath,
this.videoPath,
});
final int id;
final List<Message> messages;
final DateTime date;
final File? imagePath;
final File? videoPath;
}
class GalleryItemGrid {
GalleryItemGrid({
this.galleryItemIndex,
this.month,
this.hide,
});
final int? galleryItemIndex;
final String? month;
final bool? hide;
}
class GalleryItemThumbnail extends StatefulWidget {
const GalleryItemThumbnail({
super.key,
required this.galleryItem,
required this.onTap,
});
final GalleryItem galleryItem;
final GestureTapCallback onTap;
@override
State<GalleryItemThumbnail> createState() => _GalleryItemThumbnailState();
}
class _GalleryItemThumbnailState extends State<GalleryItemThumbnail> {
VideoPlayerController? _controller;
@override
void initState() {
super.initState();
if (widget.galleryItem.videoPath != null) {
_controller = VideoPlayerController.file(widget.galleryItem.videoPath!)
..initialize().then((_) {
setState(() {});
});
}
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
String formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
return "$twoDigitMinutes:$twoDigitSeconds";
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: widget.onTap,
child: Hero(
tag: widget.galleryItem.id,
child: (widget.galleryItem.imagePath != null)
? Image.file(widget.galleryItem.imagePath!)
: Stack(
children: [
if (_controller != null && _controller!.value.isInitialized)
Positioned.fill(
child: AspectRatio(
aspectRatio: _controller!.value.aspectRatio,
child: VideoPlayer(_controller!),
)),
if (_controller != null && _controller!.value.isInitialized)
Positioned(
bottom: 5,
right: 5,
child: Text(
_controller!.value.isInitialized
? formatDuration(_controller!.value.duration)
: '...',
style: TextStyle(
fontSize: 15, fontWeight: FontWeight.bold),
),
)
],
),
),
);
}
}
class GalleryMainView extends StatefulWidget {
const GalleryMainView({super.key});
@override
State<GalleryMainView> createState() => GalleryMainViewState();
}
class GalleryMainViewState extends State<GalleryMainView> {
bool verticalGallery = false;
List<GalleryItem> galleryItems = [];
Map<String, List<int>> orderedByMonth = {};
List<String> months = [];
@override
void initState() {
super.initState();
initAsync();
}
Future initAsync() async {
List<Message> storedMediaFiles =
await twonlyDatabase.messagesDao.getAllStoredMediaFiles();
Map<int, GalleryItem> items = {};
for (final message in storedMediaFiles) {
bool isSend = message.messageOtherId == null;
int id = message.mediaUploadId ?? message.messageId;
final basePath = await send.getMediaFilePath(
isSend ? message.mediaUploadId! : message.messageId,
isSend ? "send" : "received",
);
File? imagePath;
File? videoPath;
if (await File("$basePath.mp4").exists()) {
videoPath = File("$basePath.mp4");
} else if (await File("$basePath.png").exists()) {
imagePath = File("$basePath.png");
} else {
continue;
}
items
.putIfAbsent(
id,
() => GalleryItem(
id: id,
messages: [],
date: message.sendAt,
imagePath: imagePath,
videoPath: videoPath))
.messages
.add(message);
}
// Group items by month
orderedByMonth = {};
months = [];
String lastMonth = "";
galleryItems = items.values.toList();
for (var i = 0; i < galleryItems.length; i++) {
String month = DateFormat('MMMM yyyy').format(galleryItems[i].date);
if (lastMonth != month) {
lastMonth = month;
months.add(month);
}
orderedByMonth.putIfAbsent(month, () => []).add(i);
}
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Memories')),
body: Scrollbar(
child: ListView.builder(
itemCount: (months.length * 2),
itemBuilder: (context, mIndex) {
if (mIndex % 2 == 0) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Text(months[(mIndex / 2).toInt()]),
);
}
int index = ((mIndex - 1) / 2).toInt();
return GridView.builder(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
scrollDirection: Axis.vertical,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
childAspectRatio: 9 / 16,
),
itemCount: orderedByMonth[months[index]]!.length,
itemBuilder: (context, gIndex) {
int gaIndex = orderedByMonth[months[index]]![gIndex];
return GalleryItemThumbnail(
galleryItem: galleryItems[gaIndex],
onTap: () {
open(context, gaIndex);
},
);
},
);
},
),
),
);
}
void open(BuildContext context, final int index) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => GalleryPhotoViewWrapper(
galleryItems: galleryItems,
backgroundDecoration: const BoxDecoration(
color: Colors.black,
),
initialIndex: index,
scrollDirection: verticalGallery ? Axis.vertical : Axis.horizontal,
),
),
);
}
}
class GalleryPhotoViewWrapper extends StatefulWidget {
GalleryPhotoViewWrapper({
super.key,
this.loadingBuilder,
this.backgroundDecoration,
this.minScale,
this.maxScale,
this.initialIndex = 0,
required this.galleryItems,
this.scrollDirection = Axis.horizontal,
}) : pageController = PageController(initialPage: initialIndex);
final LoadingBuilder? loadingBuilder;
final BoxDecoration? backgroundDecoration;
final dynamic minScale;
final dynamic maxScale;
final int initialIndex;
final PageController pageController;
final List<GalleryItem> galleryItems;
final Axis scrollDirection;
@override
State<StatefulWidget> createState() {
return _GalleryPhotoViewWrapperState();
}
}
class _GalleryPhotoViewWrapperState extends State<GalleryPhotoViewWrapper> {
late int currentIndex = widget.initialIndex;
void onPageChanged(int index) {
setState(() {
currentIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: widget.backgroundDecoration,
constraints: BoxConstraints.expand(
height: MediaQuery.of(context).size.height,
),
child: Stack(
alignment: Alignment.bottomRight,
children: <Widget>[
MediaViewSizing(
bottomNavigation: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
FilledButton.icon(
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async {
await Navigator.push(
context,
PageRouteBuilder(
opaque: false,
pageBuilder: (context, a1, a2) =>
ShareImageEditorView(
videoFilePath:
widget.galleryItems[currentIndex].videoPath,
imageBytes: widget
.galleryItems[currentIndex].imagePath
?.readAsBytes(),
mirrorVideo: false,
useHighQuality: true,
),
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return child;
},
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
),
);
},
style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 30),
),
backgroundColor: WidgetStateProperty.all<Color>(
Theme.of(context).colorScheme.primary,
)),
label: Text(
context.lang.shareImagedEditorSendImage,
style: TextStyle(fontSize: 17),
),
),
],
),
child: PhotoViewGallery.builder(
scrollPhysics: const BouncingScrollPhysics(),
builder: _buildItem,
itemCount: widget.galleryItems.length,
loadingBuilder: widget.loadingBuilder,
backgroundDecoration: widget.backgroundDecoration,
pageController: widget.pageController,
onPageChanged: onPageChanged,
scrollDirection: widget.scrollDirection,
),
),
],
),
),
);
}
PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) {
final GalleryItem item = widget.galleryItems[index];
return item.videoPath != null
? PhotoViewGalleryPageOptions.customChild(
child: VideoPlayerWrapper(videoPath: item.videoPath!),
// childSize: const Size(300, 300),
initialScale: PhotoViewComputedScale.contained,
minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 4.1,
heroAttributes: PhotoViewHeroAttributes(tag: item.id),
)
: PhotoViewGalleryPageOptions(
imageProvider: FileImage(item.imagePath!),
initialScale: PhotoViewComputedScale.contained,
minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 4.1,
heroAttributes: PhotoViewHeroAttributes(tag: item.id),
);
}
}

View file

@ -3,6 +3,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:pie_menu/pie_menu.dart'; import 'package:pie_menu/pie_menu.dart';
import 'package:twonly/src/views/components/user_context_menu.dart'; import 'package:twonly/src/views/components/user_context_menu.dart';
import 'package:twonly/src/services/notification_service.dart'; import 'package:twonly/src/services/notification_service.dart';
import 'package:twonly/src/views/gallery/gallery_main_view.dart';
import 'camera/camera_preview_view.dart'; import 'camera/camera_preview_view.dart';
import 'chats/chat_list_view.dart'; import 'chats/chat_list_view.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -72,8 +73,9 @@ class HomeViewState extends State<HomeView> {
setState(() {}); setState(() {});
}, },
children: [ children: [
CameraPreviewViewPermission(),
ChatListView(), ChatListView(),
CameraPreviewViewPermission(),
GalleryMainView()
], ],
), ),
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: BottomNavigationBar(
@ -85,12 +87,16 @@ class HomeViewState extends State<HomeView> {
selectedIconTheme: IconThemeData( selectedIconTheme: IconThemeData(
color: Theme.of(context).colorScheme.inverseSurface), color: Theme.of(context).colorScheme.inverseSurface),
items: [ items: [
BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.solidComments), label: ""),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.camera), icon: FaIcon(FontAwesomeIcons.camera),
label: "", label: "",
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.solidComments), label: ""), icon: FaIcon(FontAwesomeIcons.photoFilm),
label: "",
),
], ],
onTap: (int index) { onTap: (int index) {
activePageIdx = index; activePageIdx = index;

View file

@ -1268,6 +1268,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.0" version: "6.1.0"
photo_view:
dependency: "direct main"
description:
name: photo_view
sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e"
url: "https://pub.dev"
source: hosted
version: "0.15.0"
pie_menu: pie_menu:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -68,6 +68,7 @@ dependencies:
video_player: ^2.9.5 video_player: ^2.9.5
video_compress: ^3.1.4 video_compress: ^3.1.4
share_plus: ^11.0.0 share_plus: ^11.0.0
photo_view: ^0.15.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: