diff --git a/lib/src/database/daos/messages_dao.dart b/lib/src/database/daos/messages_dao.dart index db38b7d..007d573 100644 --- a/lib/src/database/daos/messages_dao.dart +++ b/lib/src/database/daos/messages_dao.dart @@ -81,6 +81,13 @@ class MessagesDao extends DatabaseAccessor .get(); } + Future> getAllStoredMediaFiles() { + return (select(messages) + ..where((t) => t.mediaStored.equals(true)) + ..orderBy([(t) => OrderingTerm.desc(t.sendAt)])) + .get(); + } + Future> getAllMessagesPendingUploadOlderThanAMinute() { return (select(messages) ..where( diff --git a/lib/src/providers/api/media_send.dart b/lib/src/providers/api/media_send.dart index 3a2d805..7dafeb4 100644 --- a/lib/src/providers/api/media_send.dart +++ b/lib/src/providers/api/media_send.dart @@ -53,7 +53,7 @@ Future sendMediaFile( Uint8List imageBytes, bool isRealTwonly, int maxShowTime, - XFile? videoFilePath, + File? videoFilePath, bool? enableVideoAudio, bool mirrorVideo, ) async { @@ -75,7 +75,7 @@ Future sendMediaFile( if (mediaUploadId != null) { if (videoFilePath != null) { 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 handleSingleMediaFile(mediaUploadId, imageBytes); diff --git a/lib/src/views/camera/camera_preview_view.dart b/lib/src/views/camera/camera_preview_view.dart index d90a188..14c58d2 100644 --- a/lib/src/views/camera/camera_preview_view.dart +++ b/lib/src/views/camera/camera_preview_view.dart @@ -72,6 +72,7 @@ class _CameraPreviewViewState extends State { useHighQuality = user.useHighQuality!; } hasAudioPermission = await Permission.microphone.isGranted; + if (!mounted) return; setState(() {}); } @@ -155,6 +156,9 @@ class _CameraPreviewViewState extends State { } } }); + if (!mounted) { + return; + } setState(() { cameraId = sCameraId; }); @@ -239,7 +243,7 @@ class _CameraPreviewViewState extends State { } Future pushMediaEditor( - Future? imageBytes, XFile? videoFilePath) async { + Future? imageBytes, File? videoFilePath) async { bool? shoudReturn = await Navigator.push( context, PageRouteBuilder( @@ -361,16 +365,19 @@ class _CameraPreviewViewState extends State { isVideoRecording = false; sharePreviewIsShown = true; }); + File? videoPathFile; XFile? videoPath = await controller?.stopVideoRecording(); if (videoPath != null) { if (Platform.isAndroid) { // see https://github.com/flutter/flutter/issues/148335 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(); - if (await pushMediaEditor(null, videoPath)) { + if (await pushMediaEditor(null, videoPathFile)) { return; } } on CameraException catch (e) { diff --git a/lib/src/views/camera/components/save_to_gallery.dart b/lib/src/views/camera/components/save_to_gallery.dart index 987edab..d7f9169 100644 --- a/lib/src/views/camera/components/save_to_gallery.dart +++ b/lib/src/views/camera/components/save_to_gallery.dart @@ -1,4 +1,4 @@ -import 'package:camera/camera.dart'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'dart:typed_data'; @@ -8,7 +8,7 @@ import 'package:twonly/src/utils/misc.dart'; class SaveToGalleryButton extends StatefulWidget { final Future Function() getMergedImage; final String? sendNextMediaToUserName; - final XFile? videoFilePath; + final File? videoFilePath; const SaveToGalleryButton( {super.key, diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index 1df6970..97e768f 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -1,7 +1,6 @@ import 'dart:collection'; import 'dart:io'; import 'dart:async'; -import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:logging/logging.dart'; @@ -43,7 +42,7 @@ class ShareImageEditorView extends StatefulWidget { required this.useHighQuality, }); final Future? imageBytes; - final XFile? videoFilePath; + final File? videoFilePath; final Contact? sendTo; final bool mirrorVideo; final bool useHighQuality; @@ -77,8 +76,7 @@ class _ShareImageEditorView extends State { setState(() { sendingOrLoadingImage = false; }); - videoController = - VideoPlayerController.file(File(widget.videoFilePath!.path)); + videoController = VideoPlayerController.file(widget.videoFilePath!); videoController?.setLooping(true); videoController?.initialize().then((_) { videoController!.play(); diff --git a/lib/src/views/camera/share_image_view.dart b/lib/src/views/camera/share_image_view.dart index 4c5c488..c7f30ec 100644 --- a/lib/src/views/camera/share_image_view.dart +++ b/lib/src/views/camera/share_image_view.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:collection'; +import 'dart:io'; import 'dart:typed_data'; -import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; @@ -32,7 +32,7 @@ class ShareImageView extends StatefulWidget { final bool isRealTwonly; final bool mirrorVideo; final int maxShowTime; - final XFile? videoFilePath; + final File? videoFilePath; final HashSet selectedUserIds; final bool? enableVideoAudio; final Function(int, bool) updateStatus; diff --git a/lib/src/views/chats/components/chat_list_entry.dart b/lib/src/views/chats/components/chat_list_entry.dart index 5ebfba6..2fdd9ea 100644 --- a/lib/src/views/chats/components/chat_list_entry.dart +++ b/lib/src/views/chats/components/chat_list_entry.dart @@ -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/components/animate_icon.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/database/twonly_database.dart'; import 'package:twonly/src/database/tables/messages_table.dart'; @@ -251,32 +250,14 @@ class ChatListEntry extends StatelessWidget { alignment: Alignment.centerRight, child: ClipRRect( 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, - ), - ), - ); } return Align( diff --git a/lib/src/views/chats/components/in_chat_media_viewer.dart b/lib/src/views/chats/components/in_chat_media_viewer.dart index e1a989b..ab1a930 100644 --- a/lib/src/views/chats/components/in_chat_media_viewer.dart +++ b/lib/src/views/chats/components/in_chat_media_viewer.dart @@ -6,6 +6,9 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; 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/database/twonly_database.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:video_player/video_player.dart'; -class ChatMediaViewerFullScreen extends StatelessWidget { - const ChatMediaViewerFullScreen({super.key, required this.message}); +class ChatMediaViewerFullScreen extends StatefulWidget { + const ChatMediaViewerFullScreen( + {super.key, required this.message, required this.contact}); final Message message; + final Contact contact; + + @override + State createState() => + _ChatMediaViewerFullScreenState(); +} + +class _ChatMediaViewerFullScreenState extends State { + 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 Widget build(BuildContext context) { return Scaffold( - body: SafeArea( - child: Center( - child: InChatMediaViewer(message: message, isInFullscreen: true), + body: Container( + child: MediaViewSizing( + 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.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.symmetric(vertical: 10, horizontal: 30), + ), + backgroundColor: WidgetStateProperty.all( + Theme.of(context).colorScheme.primary, + )), + ), + ], + ), ), - ), + child: (hideMediaFile) + ? Container() + : InChatMediaViewer( + message: widget.message, + contact: widget.contact, + isInFullscreen: true, + ), + )), ); } } class InChatMediaViewer extends StatefulWidget { 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 Contact contact; final bool isInFullscreen; @override @@ -78,6 +162,7 @@ class _InChatMediaViewerState extends State { videoController!.setVolume(0); } videoController!.play(); + videoController!.setLooping(true); }); setState(() { @@ -100,39 +185,29 @@ class _InChatMediaViewerState extends State { videoController?.dispose(); } - Future deleteFiles() async { - await twonlyDatabase.messagesDao.updateMessageByMessageId( - widget.message.messageId, - MessagesCompanion(mediaStored: Value(false)), + Future onTap() async { + if (image == null && videoController == null) return; + if (widget.isInFullscreen) return; + bool? removed = await Navigator.push( + context, + MaterialPageRoute(builder: (context) { + return ChatMediaViewerFullScreen( + message: widget.message, contact: widget.contact); + }), ); - await send.purgeSendMediaFiles(); - await received.purgeReceivedMediaFiles(); - if (context.mounted) { - Navigator.pop(context, true); + + if (removed != null && removed) { + image = null; + videoController?.dispose(); + videoController = null; + if (isMounted) setState(() {}); } } @override Widget build(BuildContext context) { return GestureDetector( - onTap: (image == null && videoController == null) - ? null - : () async { - if (widget.isInFullscreen) return; - bool? removed = await Navigator.push( - context, - MaterialPageRoute(builder: (context) { - return ChatMediaViewerFullScreen(message: widget.message); - }), - ); - - if (removed != null && removed) { - image = null; - videoController?.dispose(); - videoController = null; - setState(() {}); - } - }, + onTap: onTap, child: Stack( children: [ if (image != null) Image.file(image!), @@ -151,22 +226,6 @@ class _InChatMediaViewerState extends State { 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"), - ) - ], - ), - ), ], ), ); diff --git a/lib/src/views/components/video_player_wrapper.dart b/lib/src/views/components/video_player_wrapper.dart new file mode 100644 index 0000000..a93b482 --- /dev/null +++ b/lib/src/views/components/video_player_wrapper.dart @@ -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 createState() => _VideoPlayerWrapperState(); +} + +class _VideoPlayerWrapperState extends State { + 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 + ); + } +} diff --git a/lib/src/views/gallery/gallery_main_view.dart b/lib/src/views/gallery/gallery_main_view.dart new file mode 100644 index 0000000..fde4bdd --- /dev/null +++ b/lib/src/views/gallery/gallery_main_view.dart @@ -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 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 createState() => _GalleryItemThumbnailState(); +} + +class _GalleryItemThumbnailState extends State { + 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 createState() => GalleryMainViewState(); +} + +class GalleryMainViewState extends State { + bool verticalGallery = false; + List galleryItems = []; + Map> orderedByMonth = {}; + List months = []; + + @override + void initState() { + super.initState(); + initAsync(); + } + + Future initAsync() async { + List storedMediaFiles = + await twonlyDatabase.messagesDao.getAllStoredMediaFiles(); + + Map 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 galleryItems; + final Axis scrollDirection; + + @override + State createState() { + return _GalleryPhotoViewWrapperState(); + } +} + +class _GalleryPhotoViewWrapperState extends State { + 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: [ + 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.symmetric(vertical: 10, horizontal: 30), + ), + backgroundColor: WidgetStateProperty.all( + 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), + ); + } +} diff --git a/lib/src/views/home_view.dart b/lib/src/views/home_view.dart index fe6b66e..3e168ec 100644 --- a/lib/src/views/home_view.dart +++ b/lib/src/views/home_view.dart @@ -3,6 +3,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:pie_menu/pie_menu.dart'; import 'package:twonly/src/views/components/user_context_menu.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 'chats/chat_list_view.dart'; import 'package:flutter/material.dart'; @@ -72,8 +73,9 @@ class HomeViewState extends State { setState(() {}); }, children: [ - CameraPreviewViewPermission(), ChatListView(), + CameraPreviewViewPermission(), + GalleryMainView() ], ), bottomNavigationBar: BottomNavigationBar( @@ -85,12 +87,16 @@ class HomeViewState extends State { selectedIconTheme: IconThemeData( color: Theme.of(context).colorScheme.inverseSurface), items: [ + BottomNavigationBarItem( + icon: FaIcon(FontAwesomeIcons.solidComments), label: ""), BottomNavigationBarItem( icon: FaIcon(FontAwesomeIcons.camera), label: "", ), BottomNavigationBarItem( - icon: FaIcon(FontAwesomeIcons.solidComments), label: ""), + icon: FaIcon(FontAwesomeIcons.photoFilm), + label: "", + ), ], onTap: (int index) { activePageIdx = index; diff --git a/pubspec.lock b/pubspec.lock index ddb866c..637e127 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1268,6 +1268,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index ce4f064..eb038a5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -68,6 +68,7 @@ dependencies: video_player: ^2.9.5 video_compress: ^3.1.4 share_plus: ^11.0.0 + photo_view: ^0.15.0 dev_dependencies: flutter_test: