diff --git a/lib/src/database/daos/messages_dao.dart b/lib/src/database/daos/messages_dao.dart index 007d573..60dbacf 100644 --- a/lib/src/database/daos/messages_dao.dart +++ b/lib/src/database/daos/messages_dao.dart @@ -81,11 +81,11 @@ class MessagesDao extends DatabaseAccessor .get(); } - Future> getAllStoredMediaFiles() { + Stream> getAllStoredMediaFiles() { return (select(messages) ..where((t) => t.mediaStored.equals(true)) ..orderBy([(t) => OrderingTerm.desc(t.sendAt)])) - .get(); + .watch(); } Future> getAllMessagesPendingUploadOlderThanAMinute() { diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 90c585d..a27dfb0 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -232,5 +232,7 @@ "additionalUsersPlusTokens": "twonly-Codes für \"Plus\"-Benutzer", "additionalUsersFreeTokens": "twonly-Codes für \"Free\"-Benutzer", "planNotAllowed": "In deinem aktuellen Plan kannst du keine Mediendateien versenden. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.", - "planLimitReached": "Du hast dein Planlimit für heute erreicht. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden." + "planLimitReached": "Du hast dein Planlimit für heute erreicht. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.", + "galleryDelete": "Datei löschen", + "galleryDetails": "Details anzeigen" } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index daf63a0..806167b 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -390,5 +390,7 @@ "additionalUsersPlusTokens": "twonly-Codes für \"Plus\" user", "additionalUsersFreeTokens": "twonly-Codes für \"Free\" user", "planLimitReached": "You have reached your plan limit for today. Upgrade your plan now to send the media file.", - "planNotAllowed": "You cannot send media files with your current tariff. Upgrade your plan now to send the media file." + "planNotAllowed": "You cannot send media files with your current tariff. Upgrade your plan now to send the media file.", + "galleryDelete": "Delete file", + "galleryDetails": "Show details" } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index d8edbc9..cdd455a 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -1408,6 +1408,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'You cannot send media files with your current tariff. Upgrade your plan now to send the media file.'** String get planNotAllowed; + + /// No description provided for @galleryDelete. + /// + /// In en, this message translates to: + /// **'Delete file'** + String get galleryDelete; + + /// No description provided for @galleryDetails. + /// + /// In en, this message translates to: + /// **'Show details'** + String get galleryDetails; } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 4d1a256..826efb5 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -682,4 +682,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get planNotAllowed => 'In deinem aktuellen Plan kannst du keine Mediendateien versenden. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.'; + + @override + String get galleryDelete => 'Datei löschen'; + + @override + String get galleryDetails => 'Details anzeigen'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 6d5eadc..6d94a15 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -682,4 +682,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get planNotAllowed => 'You cannot send media files with your current tariff. Upgrade your plan now to send the media file.'; + + @override + String get galleryDelete => 'Delete file'; + + @override + String get galleryDetails => 'Show details'; } diff --git a/lib/src/views/chats/chat_messages_components/chat_message_entry.dart b/lib/src/views/chats/chat_messages_components/chat_message_entry.dart index 8ccc9c2..7d731a4 100644 --- a/lib/src/views/chats/chat_messages_components/chat_message_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_message_entry.dart @@ -7,7 +7,7 @@ import 'package:twonly/src/views/chats/chat_messages_components/chat_message_ent import 'package:twonly/src/views/chats/chat_messages_components/sliding_response.dart'; import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/model/json/message.dart'; -import 'package:twonly/src/views/gallery/gallery_main_view.dart'; +import 'package:twonly/src/views/gallery/gallery_item.dart'; class ChatListEntry extends StatefulWidget { const ChatListEntry( diff --git a/lib/src/views/chats/chat_messages_components/chat_message_entry_components/chat_media_entry.dart b/lib/src/views/chats/chat_messages_components/chat_message_entry_components/chat_media_entry.dart index 8c6e04e..7f901a9 100644 --- a/lib/src/views/chats/chat_messages_components/chat_message_entry_components/chat_media_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_message_entry_components/chat_media_entry.dart @@ -9,7 +9,7 @@ import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/api/media_received.dart' as received; import 'package:twonly/src/services/notification_service.dart'; import 'package:twonly/src/views/chats/media_viewer_view.dart'; -import 'package:twonly/src/views/gallery/gallery_main_view.dart'; +import 'package:twonly/src/views/gallery/gallery_item.dart'; class ChatMediaEntry extends StatelessWidget { const ChatMediaEntry({ diff --git a/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart b/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart index 8460a07..6c96c75 100644 --- a/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart +++ b/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart @@ -8,7 +8,8 @@ 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'; import 'package:twonly/src/model/json/message.dart'; -import 'package:twonly/src/views/gallery/gallery_main_view.dart'; +import 'package:twonly/src/views/gallery/gallery_item.dart'; +import 'package:twonly/src/views/gallery/gallery_photo_view.dart'; import 'package:video_player/video_player.dart'; class InChatMediaViewer extends StatefulWidget { diff --git a/lib/src/views/chats/chat_messages_view.dart b/lib/src/views/chats/chat_messages_view.dart index f4e6a6c..af93edf 100644 --- a/lib/src/views/chats/chat_messages_view.dart +++ b/lib/src/views/chats/chat_messages_view.dart @@ -18,7 +18,7 @@ import 'package:twonly/src/services/notification_service.dart'; import 'package:twonly/src/views/camera/camera_send_to_view.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/contact/contact_view.dart'; -import 'package:twonly/src/views/gallery/gallery_main_view.dart'; +import 'package:twonly/src/views/gallery/gallery_item.dart'; Color getMessageColor(Message message) { return (message.messageOtherId == null) diff --git a/lib/src/views/gallery/gallery_item.dart b/lib/src/views/gallery/gallery_item.dart new file mode 100644 index 0000000..1df192b --- /dev/null +++ b/lib/src/views/gallery/gallery_item.dart @@ -0,0 +1,71 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:drift/drift.dart'; +import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/providers/api/media_send.dart' as send; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/twonly_database.dart'; + +class GalleryItem { + GalleryItem({ + required this.id, + required this.messages, + required this.date, + required this.mirrorVideo, + this.imagePath, + this.videoPath, + }); + final String id; + final bool mirrorVideo; + final List messages; + final DateTime date; + final File? imagePath; + final File? videoPath; + + static Future> convertFromMessages( + List messages) async { + Map items = {}; + for (final message in messages) { + 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 { + if (message.mediaStored) { + /// media file was deleted, ... remove the file + twonlyDatabase.messagesDao.updateMessageByMessageId( + message.messageId, MessagesCompanion(mediaStored: Value(false))); + } + continue; + } + bool mirrorVideo = false; + if (videoPath != null) { + MediaMessageContent content = + MediaMessageContent.fromJson(jsonDecode(message.contentJson!)); + mirrorVideo = content.mirrorVideo; + } + + items + .putIfAbsent( + id, + () => GalleryItem( + id: id.toString(), + messages: [], + date: message.sendAt, + mirrorVideo: mirrorVideo, + imagePath: imagePath, + videoPath: videoPath)) + .messages + .add(message); + } + return items; + } +} diff --git a/lib/src/views/gallery/gallery_item_thumbnail.dart b/lib/src/views/gallery/gallery_item_thumbnail.dart new file mode 100644 index 0000000..e882fb2 --- /dev/null +++ b/lib/src/views/gallery/gallery_item_thumbnail.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/views/gallery/gallery_item.dart'; +import 'package:video_player/video_player.dart'; + +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), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/src/views/gallery/gallery_main_view.dart b/lib/src/views/gallery/gallery_main_view.dart index ec2d9f0..c1ac94a 100644 --- a/lib/src/views/gallery/gallery_main_view.dart +++ b/lib/src/views/gallery/gallery_main_view.dart @@ -1,172 +1,13 @@ -import 'dart:convert'; +import 'dart:async'; import 'dart:io'; -import 'package:drift/drift.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:intl/intl.dart'; -import 'package:twonly/src/model/json/message.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, - required this.mirrorVideo, - this.imagePath, - this.videoPath, - }); - final String id; - final bool mirrorVideo; - final List messages; - final DateTime date; - final File? imagePath; - final File? videoPath; - - static Future> convertFromMessages( - List messages) async { - Map items = {}; - for (final message in messages) { - 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 { - if (message.mediaStored) { - /// media file was deleted, ... remove the file - twonlyDatabase.messagesDao.updateMessageByMessageId( - message.messageId, MessagesCompanion(mediaStored: Value(false))); - } - continue; - } - bool mirrorVideo = false; - if (videoPath != null) { - MediaMessageContent content = - MediaMessageContent.fromJson(jsonDecode(message.contentJson!)); - mirrorVideo = content.mirrorVideo; - } - - items - .putIfAbsent( - id, - () => GalleryItem( - id: id.toString(), - messages: [], - date: message.sendAt, - mirrorVideo: mirrorVideo, - imagePath: imagePath, - videoPath: videoPath)) - .messages - .add(message); - } - return items; - } -} - -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), - ), - ) - ], - ), - ), - ); - } -} +import 'package:twonly/src/views/gallery/gallery_item.dart'; +import 'package:twonly/src/views/gallery/gallery_item_thumbnail.dart'; +import 'package:twonly/src/views/gallery/gallery_photo_view.dart'; class GalleryMainView extends StatefulWidget { const GalleryMainView({super.key}); @@ -181,6 +22,7 @@ class GalleryMainViewState extends State { Map> orderedByMonth = {}; List months = []; bool mounted = true; + StreamSubscription>? messageSub; @override void initState() { @@ -191,6 +33,7 @@ class GalleryMainViewState extends State { @override void dispose() { mounted = false; + messageSub?.cancel(); super.dispose(); } @@ -230,30 +73,31 @@ class GalleryMainViewState extends State { } Future initAsync() async { - List storedMediaFiles = - await twonlyDatabase.messagesDao.getAllStoredMediaFiles(); + messageSub?.cancel(); + Stream> msgStream = + twonlyDatabase.messagesDao.getAllStoredMediaFiles(); - Map items = - await GalleryItem.convertFromMessages(storedMediaFiles); - - // Group items by month - orderedByMonth = {}; - months = []; - String lastMonth = ""; - galleryItems = await loadMemoriesDirectory(); - galleryItems += items.values.toList(); - galleryItems.sort((a, b) => b.date.compareTo(a.date)); - 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); + messageSub = msgStream.listen((msgs) async { + Map items = await GalleryItem.convertFromMessages(msgs); + // Group items by month + orderedByMonth = {}; + months = []; + String lastMonth = ""; + galleryItems = await loadMemoriesDirectory(); + galleryItems += items.values.toList(); + galleryItems.sort((a, b) => b.date.compareTo(a.date)); + 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); } - orderedByMonth.putIfAbsent(month, () => []).add(i); - } - if (mounted) { - setState(() {}); - } + if (mounted) { + setState(() {}); + } + }); } @override @@ -296,144 +140,17 @@ class GalleryMainViewState extends State { ); } - void open(BuildContext context, final int index) { - Navigator.push( + void open(BuildContext context, final int index) async { + await 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 { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ShareImageEditorView( - videoFilePath: - widget.galleryItems[currentIndex].videoPath, - imageBytes: widget - .galleryItems[currentIndex].imagePath - ?.readAsBytes(), - mirrorVideo: false, - useHighQuality: true, - ), - ), - ); - }, - 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!, - mirrorVideo: item.mirrorVideo, - ), - // 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), - ); + initAsync(); } } diff --git a/lib/src/views/gallery/gallery_photo_view.dart b/lib/src/views/gallery/gallery_photo_view.dart new file mode 100644 index 0000000..6338884 --- /dev/null +++ b/lib/src/views/gallery/gallery_photo_view.dart @@ -0,0 +1,190 @@ +import 'package:drift/drift.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +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/providers/api/media_received.dart' as received; +import 'package:twonly/src/providers/api/media_send.dart' as send; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/camera/share_image_editor_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/video_player_wrapper.dart'; +import 'package:twonly/src/views/gallery/gallery_item.dart'; + +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; + }); + } + + Future deleteFile() async { + List messages = widget.galleryItems[currentIndex].messages; + bool confirmed = await showAlertDialog( + context, "Are you sure?", "The image will be irrevocably deleted."); + + if (!confirmed) return; + + widget.galleryItems[currentIndex].imagePath?.deleteSync(); + widget.galleryItems[currentIndex].videoPath?.deleteSync(); + for (final message in messages) { + await twonlyDatabase.messagesDao.updateMessageByMessageId( + message.messageId, + MessagesCompanion(mediaStored: Value(false)), + ); + } + + widget.galleryItems.removeAt(currentIndex); + setState(() {}); + await send.purgeSendMediaFiles(); + await received.purgeReceivedMediaFiles(); + if (context.mounted) { + Navigator.pop(context, true); + } + } + + @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 { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ShareImageEditorView( + videoFilePath: + widget.galleryItems[currentIndex].videoPath, + imageBytes: widget + .galleryItems[currentIndex].imagePath + ?.readAsBytes(), + mirrorVideo: false, + useHighQuality: true, + ), + ), + ); + }, + 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: Stack( + children: [ + 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, + ), + Positioned( + right: 5, + child: PopupMenuButton( + onSelected: (String result) { + if (result == "delete") { + deleteFile(); + } + }, + itemBuilder: (BuildContext context) => + >[ + PopupMenuItem( + value: 'delete', + child: Text(context.lang.galleryDelete), + ), + // PopupMenuItem( + // value: 'details', + // child: Text(context.lang.galleryDetails), + // ), + ], + ), + ) + ], + ), + ), + ], + ), + ), + ); + } + + PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) { + final GalleryItem item = widget.galleryItems[index]; + return item.videoPath != null + ? PhotoViewGalleryPageOptions.customChild( + child: VideoPlayerWrapper( + videoPath: item.videoPath!, + mirrorVideo: item.mirrorVideo, + ), + // 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), + ); + } +}